init commit

This commit is contained in:
Matt Batchelder
2026-02-11 20:55:38 -05:00
commit 6e3929c459
2240 changed files with 467828 additions and 0 deletions

View File

@@ -0,0 +1,188 @@
<?php
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
/**
* Class ApplicationState
* @package Xibo\Helper
*/
class ApplicationState
{
public $httpStatus = 200;
public $template;
public $message;
public $success;
public $html;
public $buttons;
public $fieldActions;
public $dialogTitle;
public $callBack;
public $autoSubmit;
public $login;
public $clockUpdate;
public $id;
private $data;
public $extra;
public $recordsTotal;
public $recordsFiltered;
private $commit = true;
public function __construct()
{
// Assume success
$this->success = true;
$this->buttons = '';
$this->fieldActions = '';
$this->extra = array();
}
/**
* Sets the Default response if for a login box
*/
public static function asRequiresLogin()
{
return [
'login' => true,
'success' => false
];
}
/**
* Add a Field Action to a Field
* @param string $field The field name
* @param string $action The action name
* @param string $value The value to trigger on
* @param string $actions The actions (field => action)
* @param string $operation The Operation (optional)
*/
public function addFieldAction($field, $action, $value, $actions, $operation = "equals")
{
$this->fieldActions[] = array(
'field' => $field,
'trigger' => $action,
'value' => $value,
'operation' => $operation,
'actions' => $actions
);
}
/**
* Response JSON
* @return array
*/
public function asArray()
{
// Construct the Response
$response = array();
// General
$response['html'] = $this->html;
$response['buttons'] = $this->buttons;
$response['fieldActions'] = $this->fieldActions;
$response['dialogTitle'] = $this->dialogTitle;
$response['callBack'] = $this->callBack;
$response['autoSubmit'] = $this->autoSubmit;
$response['success'] = $this->success;
$response['message'] = $this->message;
$response['clockUpdate'] = $this->clockUpdate;
// Login
$response['login'] = $this->login;
// Extra
$response['id'] = intval($this->id);
$response['extra'] = $this->extra;
$response['data'] = $this->data;
return $response;
}
/**
* @return false|string
*/
public function asJson()
{
return json_encode($this->asArray());
}
/**
* Set Data
* @param array $data
*/
public function setData($data)
{
$this->data = $data;
}
/**
* Get Data
* @return array|mixed
*/
public function getData()
{
if ($this->data == null) {
$this->data = [];
}
return $this->data;
}
/**
* Hydrate with properties
*
* @param array $properties
*
* @return self
*/
public function hydrate(array $properties)
{
foreach ($properties as $prop => $val) {
if (property_exists($this, $prop)) {
$this->{$prop} = $val;
}
}
return $this;
}
/**
* Called in the Storage Middleware to determine whether or not we should commit this transaction.
* @return bool
*/
public function getCommitState()
{
return $this->commit;
}
/**
* Set the commit state
* @param bool $state
* @return bool
*/
public function setCommitState(bool $state)
{
return $this->commit = $state;
}
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
/**
* Class AttachmentUploadHandler
* @package Xibo\Helper
*/
class AttachmentUploadHandler extends BlueImpUploadHandler
{
/**
* @param $file
* @param $index
*/
protected function handleFormData($file, $index)
{
$controller = $this->options['controller'];
/* @var \Xibo\Controller\Notification $controller */
$controller->getLog()->debug('Upload complete for name: ' . $file->name);
}
}

View File

@@ -0,0 +1,500 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use Psr\Log\LoggerInterface;
/**
* Heavily modified BlueImp Upload handler, stripped out image processing, downloads, etc.
* jQuery File Upload Plugin PHP Class 6.4.2
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2010, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
class BlueImpUploadHandler
{
protected array $options;
// PHP File Upload error message codes:
// http://php.net/manual/en/features.file-upload.errors.php
private array $errorMessages = [
1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
3 => 'The uploaded file was only partially uploaded',
4 => 'No file was uploaded',
6 => 'Missing a temporary folder',
7 => 'Failed to write file to disk',
8 => 'A PHP extension stopped the file upload',
'post_max_size' => 'The uploaded file exceeds the post_max_size directive in php.ini',
'accept_file_types' => 'Filetype not allowed',
];
/**
* @param string $uploadDir
* @param \Psr\Log\LoggerInterface $logger
* @param array $options
* @param bool $initialize
*/
public function __construct(
string $uploadDir,
private readonly LoggerInterface $logger,
array $options = [],
bool $initialize = true,
) {
$this->options = array_merge([
'upload_dir' => $uploadDir,
'access_control_allow_origin' => '*',
'access_control_allow_methods' => array(
'OPTIONS',
'HEAD',
'GET',
'POST',
'PUT',
'PATCH',
'DELETE'
),
'access_control_allow_headers' => array(
'Content-Type',
'Content-Range',
'Content-Disposition'
),
// Defines which files can be displayed inline when downloaded:
'inline_file_types' => '/\.(gif|jpe?g|png)$/i',
// Defines which files (based on their names) are accepted for upload:
'accept_file_types' => '/.+$/i',
// Set the following option to false to enable resumable uploads:
'discard_aborted_uploads' => true,
], $options);
if ($initialize) {
$this->initialize();
}
}
protected function getLogger(): LoggerInterface
{
return $this->logger;
}
private function initialize(): void
{
switch ($this->getServerVar('REQUEST_METHOD')) {
case 'OPTIONS':
case 'HEAD':
$this->head();
break;
case 'PATCH':
case 'PUT':
case 'POST':
$this->post();
break;
default:
$this->header('HTTP/1.1 405 Method Not Allowed');
}
}
/**
* Get the upload directory
* @return string
*/
protected function getUploadDir(): string
{
return $this->options['upload_dir'];
}
/**
* @param $fileName
* @param $version
* @return string
*/
private function getUploadPath($fileName = null, $version = null): string
{
$this->getLogger()->debug('getUploadPath: ' . $fileName);
$fileName = $fileName ?: '';
$versionPath = empty($version) ? '' : $version . '/';
return $this->options['upload_dir'] . $versionPath . $fileName;
}
/**
* Fix for overflowing signed 32-bit integers,
* works for sizes up to 2^32-1 bytes (4 GiB - 1):
* @param $size
* @return int
*/
private function fixIntegerOverflow($size): int
{
if ($size < 0) {
$size += 2.0 * (PHP_INT_MAX + 1);
}
return $size;
}
/**
* @param string $filePath
* @param bool $clearStatCache
* @return int
*/
private function getFileSize(string $filePath, bool $clearStatCache = false): int
{
if ($clearStatCache) {
clearstatcache(true, $filePath);
}
return $this->fixIntegerOverflow(filesize($filePath));
}
/**
* @param $error
* @return string
*/
private function getErrorMessage($error): string
{
return $this->errorMessages[$error] ?? $error;
}
/**
* @param $val
* @return float|int
*/
private function getConfigBytes($val): float|int
{
return $this->fixIntegerOverflow(ByteFormatter::toBytes($val));
}
/**
* @param $file
* @param $error
* @return bool
*/
private function validate($file, $error): bool
{
if ($error) {
$file->error = $this->getErrorMessage($error);
return false;
}
// Make sure the content length isn't greater than the max size
$contentLength = $this->fixIntegerOverflow(intval($this->getServerVar('CONTENT_LENGTH')));
$postMaxSize = $this->getConfigBytes(ini_get('post_max_size'));
if ($postMaxSize && ($contentLength > $postMaxSize)) {
$file->error = $this->getErrorMessage('post_max_size');
return false;
}
// Max sure the we are an accepted file type
if (!preg_match($this->options['accept_file_types'], $file->name)) {
$file->error = $this->getErrorMessage('accept_file_types');
return false;
}
return true;
}
private function upcountName(string $name): string
{
$this->getLogger()->debug('upcountName: ' . $name);
return preg_replace_callback(
'/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/',
function ($matches): string {
$this->getLogger()->debug('upcountName: callback, matches: ' . var_export($matches, true));
$index = isset($matches[1]) ? intval($matches[1]) + 1 : 1;
$ext = $matches[2] ?? '';
return ' (' . $index . ')' . $ext;
},
$name,
1
);
}
/**
* @param $name
* @param $contentRange
* @return string
*/
private function getUniqueFilename($name, $contentRange): string
{
$uploadPath = $this->getUploadPath($name);
$this->getLogger()->debug('getUniqueFilename: ' . $name . ', uploadPath: ' . $uploadPath
. ', contentRange: ' . $contentRange);
$attempts = 0;
while (is_dir($uploadPath) && $attempts < 100) {
$name = $this->upcountName($name);
$attempts++;
}
$this->getLogger()->debug('getUniqueFilename: resolved file path: ' . $name);
$contentRange = $contentRange === null ? 0 : $contentRange[1];
// Keep an existing filename if this is part of a chunked upload:
$uploaded_bytes = $this->fixIntegerOverflow($contentRange);
while (is_file($this->getUploadPath($name))) {
if ($uploaded_bytes === $this->getFileSize($this->getUploadPath($name))) {
break;
}
$name = $this->upcountName($name);
}
return $name;
}
/**
* @param $name
* @param $type
* @return string
*/
private function trimFileName($name, $type): string
{
// Remove path information and dots around the filename, to prevent uploading
// into different directories or replacing hidden system files.
// Also remove control characters and spaces (\x00..\x20) around the filename:
$name = trim(basename(stripslashes($name)), ".\x00..\x20");
// Use a timestamp for empty filenames:
if (!$name) {
$name = str_replace('.', '-', microtime(true));
}
// Add missing file extension for known image types:
if (!str_contains($name, '.')
&& preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)
) {
$name .= '.' . $matches[1];
}
return $name;
}
/**
* @param string $name
* @param string $type
* @param int|null $contentRange
* @return string
*/
private function getFileName(string $name, string $type, ?int $contentRange): string
{
$this->getLogger()->debug('getFileName: ' . $name . ', type: ' . $type);
return $this->getUniqueFilename(
$this->trimFileName($name, $type),
$contentRange
);
}
/**
* @param $uploadedFile
* @param $name
* @param $size
* @param $type
* @param $error
* @param $index
* @param $contentRange
* @return \stdClass
*/
private function handleFileUpload(
$uploadedFile,
$name,
$size,
$type,
$error,
$index = null,
$contentRange = null
) {
$this->getLogger()->debug('handleFileUpload: ' . $uploadedFile);
// Build a file object to return.
$file = new \stdClass();
$file->name = $this->getFileName($name, $type, $contentRange);
$file->size = $this->fixIntegerOverflow(intval($size));
$file->type = $type;
if ($this->validate($file, $error)) {
$uploadPath = $this->getUploadPath();
if (!is_dir($uploadPath)) {
mkdir($uploadPath, 0755, true);
}
$filePath = $this->getUploadPath($file->name);
// Are we appending?
$appendFile = $contentRange && is_file($filePath) && $file->size > $this->getFileSize($filePath);
if ($uploadedFile && is_uploaded_file($uploadedFile)) {
// multipart/formdata uploads (POST method uploads)
if ($appendFile) {
file_put_contents(
$filePath,
fopen($uploadedFile, 'r'),
FILE_APPEND
);
} else {
move_uploaded_file($uploadedFile, $filePath);
}
} else {
// Non-multipart uploads (PUT method support)
file_put_contents(
$filePath,
fopen('php://input', 'r'),
$appendFile ? FILE_APPEND : 0
);
}
$fileSize = $this->getFileSize($filePath, $appendFile);
if ($fileSize === $file->size) {
$this->handleFormData($file, $index);
} else {
$file->size = $fileSize;
if (!$contentRange && $this->options['discard_aborted_uploads']) {
unlink($filePath);
$file->error = 'abort';
}
}
}
return $file;
}
/**
* @param $file
* @param $index
* @return void
*/
protected function handleFormData($file, $index)
{
}
/**
* @param string $str
* @return void
*/
private function header(string $str): void
{
header($str);
}
/**
* @param $id
* @return mixed|string
*/
private function getServerVar($id): mixed
{
return $_SERVER[$id] ?? '';
}
private function sendContentTypeHeader(): void
{
$this->header('Vary: Accept');
if (str_contains($this->getServerVar('HTTP_ACCEPT'), 'application/json')) {
$this->header('Content-type: application/json');
} else {
$this->header('Content-type: text/plain');
}
}
private function sendAccessControlHeaders(): void
{
$this->header('Access-Control-Allow-Origin: ' . $this->options['access_control_allow_origin']);
$this->header('Access-Control-Allow-Methods: '
. implode(', ', $this->options['access_control_allow_methods']));
$this->header('Access-Control-Allow-Headers: '
. implode(', ', $this->options['access_control_allow_headers']));
}
private function head(): void
{
$this->header('Pragma: no-cache');
$this->header('Cache-Control: no-store, no-cache, must-revalidate');
$this->header('Content-Disposition: inline; filename="files.json"');
// Prevent Internet Explorer from MIME-sniffing the content-type:
$this->header('X-Content-Type-Options: nosniff');
if ($this->options['access_control_allow_origin']) {
$this->sendAccessControlHeaders();
}
$this->sendContentTypeHeader();
}
/**
* @return void
*/
public function post(): void
{
$upload = $_FILES['files'] ?? null;
// Parse the Content-Disposition header, if available:
$fileName = $this->getServerVar('HTTP_CONTENT_DISPOSITION') ?
rawurldecode(preg_replace(
'/(^[^"]+")|("$)/',
'',
$this->getServerVar('HTTP_CONTENT_DISPOSITION')
)) : null;
// Parse the Content-Range header, which has the following form:
// Content-Range: bytes 0-524287/2000000
$contentRange = $this->getServerVar('HTTP_CONTENT_RANGE')
? preg_split('/[^0-9]+/', $this->getServerVar('HTTP_CONTENT_RANGE'))
: null;
$size = $contentRange ? $contentRange[3] : null;
$this->getLogger()->debug('post: contentRange: ' . var_export($contentRange, true));
$files = [];
if ($upload && is_array($upload['tmp_name'])) {
// param_name is an array identifier like "files[]",
// $_FILES is a multi-dimensional array:
foreach ($upload['tmp_name'] as $index => $value) {
$files[] = $this->handleFileUpload(
$upload['tmp_name'][$index],
$fileName ?: $upload['name'][$index],
$size ?: $upload['size'][$index],
$upload['type'][$index],
$upload['error'][$index],
$index,
$contentRange
);
}
} else {
// param_name is a single object identifier like "file",
// $_FILES is a one-dimensional array:
$files[] = $this->handleFileUpload(
$upload['tmp_name'] ?? null,
$fileName ?: ($upload['name'] ?? null),
$size ?: ($upload['size'] ?? $this->getServerVar('CONTENT_LENGTH')),
$upload['type'] ?? $this->getServerVar('CONTENT_TYPE'),
$upload['error'] ?? null,
null,
$contentRange
);
}
// Output response
$json = json_encode(['files' => $files]);
$this->head();
if ($this->getServerVar('HTTP_CONTENT_RANGE')) {
if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) {
$this->header('Range: 0-' . (
$this->fixIntegerOverflow(intval($files[0]->size)) - 1
));
}
}
echo $json;
}
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* Spring Signage Ltd - http://www.springsignage.com
* Copyright (C) 2015 Spring Signage Ltd
* (ByteFormatter.php)
*/
namespace Xibo\Helper;
/**
* Class ByteFormatter
* @package Xibo\Helper
*/
class ByteFormatter
{
/**
* Format Bytes
* http://stackoverflow.com/questions/2510434/format-bytes-to-kilobytes-megabytes-gigabytes
* @param int $size The file size in bytes
* @param int $precision The precision to go to
* @param bool $si Use SI units or not
* @return string The Formatted string with suffix
*/
public static function format($size, $precision = 2, $si = false)
{
if ($size <= 0) {
return 0;
}
if ($si === false) {
// IEC prefixes (binary)
$suffixes = array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB');
$mod = 1024;
$base = log($size) / log($mod);
} else {
// SI prefixes (decimal)
$suffixes = array('B', 'kB', 'MB', 'GB', 'TB', 'PB');
$mod = 1000;
$base = log($size) / log($mod);
}
return round(pow($mod, $base - floor($base)), $precision) . ' ' . $suffixes[(int)floor($base)];
}
/**
* @param $val
* @return int|string
*/
public static function toBytes($val) {
$val = trim($val);
$last = strtolower($val[strlen($val)-1]);
$val = substr($val, 0, -1);
switch($last) {
// The 'G' modifier is available since PHP 5.1.0
case 'g':
$val *= 1024;
case 'm':
$val *= 1024;
case 'k':
$val *= 1024;
}
return $val;
}
}

View File

@@ -0,0 +1,176 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use Carbon\Carbon;
use Exception;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* Class DataSetUploadHandler
* @package Xibo\Helper
*/
class DataSetUploadHandler extends BlueImpUploadHandler
{
/**
* @param $file
* @param $index
*/
protected function handleFormData($file, $index)
{
/* @var \Xibo\Controller\DataSet $controller */
$controller = $this->options['controller'];
/* @var SanitizerInterface $sanitizer */
$sanitizer = $this->options['sanitizer'];
// Handle form data, e.g. $_REQUEST['description'][$index]
$fileName = $file->name;
$controller->getLog()->debug('Upload complete for ' . $fileName . '.');
// Upload and Save
try {
// Authenticate
$controller = $this->options['controller'];
$dataSet = $controller->getDataSetFactory()->getById($this->options['dataSetId']);
if (!$controller->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
// Get all columns
$columns = $dataSet->getColumn();
// Filter columns where dataSetColumnType is "Value"
$filteredColumns = array_filter($columns, function ($column) {
return $column->dataSetColumnTypeId == '1';
});
// Check if there are any value columns defined in the dataset
if (count($filteredColumns) === 0) {
$controller->getLog()->error('Import failed: No value columns defined in the dataset.');
throw new InvalidArgumentException(__('Import failed: No value columns defined in the dataset.'));
}
// We are allowed to edit - pull all required parameters from the request object
$overwrite = $sanitizer->getCheckbox('overwrite');
$ignoreFirstRow = $sanitizer->getCheckbox('ignorefirstrow');
$controller->getLog()->debug('Options provided - overwrite = %d, ignore first row = %d', $overwrite, $ignoreFirstRow);
// Enumerate over the columns in the DataSet and set a row value for each
$spreadSheetMapping = [];
foreach ($dataSet->getColumn() as $column) {
/* @var \Xibo\Entity\DataSetColumn $column */
if ($column->dataSetColumnTypeId == 1) {
// Has this column been provided in the mappings?
$spreadSheetColumn = 0;
if (isset($_REQUEST['csvImport_' . $column->dataSetColumnId]))
$spreadSheetColumn = (($index === null) ? $_REQUEST['csvImport_' . $column->dataSetColumnId] : $_REQUEST['csvImport_' . $column->dataSetColumnId][$index]);
// If it has been left blank, then skip
if ($spreadSheetColumn != 0)
$spreadSheetMapping[($spreadSheetColumn - 1)] = $column->heading;
}
}
// Delete the data?
if ($overwrite == 1)
$dataSet->deleteData();
$firstRow = true;
$i = 0;
$handle = fopen($controller->getConfig()->getSetting('LIBRARY_LOCATION') . 'temp/' . $fileName, 'r');
while (($data = fgetcsv($handle)) !== FALSE ) {
$i++;
// remove any elements that doesn't contain any value from the array
$filteredData = array_filter($data, function($value) {
return !empty($value);
});
// Skip empty lines without any delimiters or data
if (empty($filteredData)) {
continue;
}
$row = [];
// The CSV file might have headings, so ignore the first row.
if ($firstRow) {
$firstRow = false;
if ($ignoreFirstRow == 1)
continue;
}
for ($cell = 0; $cell < count($data); $cell++) {
// Insert the data into the correct column
if (isset($spreadSheetMapping[$cell])) {
// Sanitize the data a bit
$item = $data[$cell];
if ($item == '')
$item = null;
$row[$spreadSheetMapping[$cell]] = $item;
}
}
try {
$dataSet->addRow($row);
} catch (\PDOException $PDOException) {
$controller->getLog()->error('Error importing row ' . $i . '. E = ' . $PDOException->getMessage());
$controller->getLog()->debug($PDOException->getTraceAsString());
throw new InvalidArgumentException(__('Unable to import row %d', $i), 'row');
}
}
// Close the file
fclose($handle);
// TODO: update list content definitions
// Save the dataSet
$dataSet->lastDataEdit = Carbon::now()->format('U');
$dataSet->save(['validate' => false, 'saveColumns' => false]);
// Tidy up the temporary file
@unlink($controller->getConfig()->getSetting('LIBRARY_LOCATION') . 'temp/' . $fileName);
} catch (Exception $e) {
$file->error = $e->getMessage();
// Don't commit
$controller->getState()->setCommitState(false);
}
}
}

View File

@@ -0,0 +1,186 @@
<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger;
use Xibo\Storage\PdoStorageService;
/**
* Class DatabaseLogHandler
* @package Xibo\Helper
*/
class DatabaseLogHandler extends AbstractProcessingHandler
{
/** @var \PDO */
private static $pdo;
/** @var \PDOStatement|null */
private static $statement;
/** @var int Log Level */
protected $level = Logger::ERROR;
/** @var int Track the number of failures since a success */
private $failureCount = 0;
/**
* @param int $level The minimum logging level at which this handler will be triggered
*/
public function __construct($level = Logger::ERROR)
{
parent::__construct($level);
}
/**
* Gets minimum logging level at which this handler will be triggered.
*
* @return int
*/
public function getLevel(): int
{
return $this->level;
}
/**
* @param int|string $level
* @return $this|\Monolog\Handler\AbstractHandler
*/
public function setLevel($level): \Monolog\Handler\AbstractHandler
{
$this->level = Logger::toMonologLevel($level);
return $this;
}
/**
* @inheritDoc
* @throws \Exception
*/
protected function write(array $record): void
{
if (self::$statement == null) {
self::$pdo = PdoStorageService::newConnection('log');
$SQL = '
INSERT INTO `log` (
`runNo`,
`logdate`,
`channel`,
`type`,
`page`,
`function`,
`message`,
`userid`,
`displayid`,
`sessionHistoryId`,
`requestId`
) VALUES (
:runNo,
:logdate,
:channel,
:type,
:page,
:function,
:message,
:userid,
:displayid,
:sessionHistoryId,
:requestId
)
';
self::$statement = self::$pdo->prepare($SQL);
}
$params = [
'runNo' => $record['extra']['uid'] ?? '',
'logdate' => $record['datetime']->format('Y-m-d H:i:s'),
'type' => $record['level_name'],
'channel' => $record['channel'],
'page' => $record['extra']['route'] ?? '',
'function' => $record['extra']['method'] ?? '',
'message' => $record['message'],
'userid' => $record['extra']['userId'] ?? 0,
'displayid' => $record['extra']['displayId'] ?? 0,
'sessionHistoryId' => $record['extra']['sessionHistoryId'] ?? 0,
'requestId' => $record['extra']['requestId'] ?? 0,
];
try {
// Insert
self::$statement->execute($params);
// Reset failure count
$this->failureCount = 0;
// Successful write
PdoStorageService::incrementStat('log', 'insert');
} catch (\Exception $e) {
// Increment failure count
$this->failureCount++;
// Try to create a new statement
if ($this->failureCount <= 1) {
// Clear the stored statement, and try again
// this will rebuild the connection
self::$statement = null;
// Try again.
$this->write($record);
}
// If the failureCount is > 1, then we ignore the error.
}
}
/**
* Deleting logs must happen on the same DB connection as the log handler writes logs
* otherwise we can end up with a deadlock where the log handler has written things, locked the table
* and, we're then trying to get the same lock.
* @param string $cutOff
*/
public static function tidyLogs(string $cutOff): void
{
try {
if (self::$pdo === null) {
self::$pdo = PdoStorageService::newConnection('log');
}
$statement = self::$pdo->prepare('DELETE FROM `log` WHERE logdate < :maxage LIMIT 10000');
do {
// Execute statement
$statement->execute(['maxage' => $cutOff]);
// initialize number of rows deleted
$rowsDeleted = $statement->rowCount();
PdoStorageService::incrementStat('log', 'delete');
// pause for a second
sleep(2);
} while ($rowsDeleted > 0);
} catch (\PDOException) {
}
}
}

View File

@@ -0,0 +1,265 @@
<?php
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use Carbon\Carbon;
/**
* Class Environment
* @package Xibo\Helper
*/
class DateFormatHelper
{
private static $timezones = null;
/**
* Get the default date format
* @return string
*/
public static function getSystemFormat()
{
return 'Y-m-d H:i:s';
}
/**
* @inheritdoc
*/
public static function extractTimeFormat($format)
{
$replacements = [
'd' => '',
'D' => '',
'j' => '',
'l' => '',
'N' => '',
'S' => '',
'w' => '',
'z' => '',
'W' => '',
'F' => '',
'm' => '',
'M' => '',
'n' => '',
't' => '', // no equivalent
'L' => '', // no equivalent
'o' => '',
'Y' => '',
'y' => '',
'a' => 'a',
'A' => 'A',
'B' => '', // no equivalent
'g' => 'g',
'G' => 'G',
'h' => 'h',
'H' => 'H',
'i' => 'i',
's' => 's',
'u' => '',
'e' => '', // deprecated since version 1.6.0 of moment.js
'I' => '', // no equivalent
'O' => '', // no equivalent
'P' => '', // no equivalent
'T' => '', // no equivalent
'Z' => '', // no equivalent
'c' => '', // no equivalent
'r' => '', // no equivalent
'U' => '',
'-' => '',
'/' => '',
'.' => ''
];
$timeOnly = strtr($format, $replacements);
return trim($timeOnly);
}
/**
* @inheritdoc
*/
public static function extractDateOnlyFormat($format)
{
$replacements = [
'd' => 'd',
'D' => 'D',
'j' => '',
'l' => '',
'N' => '',
'S' => '',
'w' => '',
'z' => '',
'W' => '',
'F' => '',
'm' => 'm',
'M' => 'M',
'n' => '',
't' => '', // no equivalent
'L' => '', // no equivalent
'o' => '',
'Y' => 'Y',
'y' => 'y',
'a' => '',
'A' => '',
'B' => '', // no equivalent
'g' => '',
'G' => '',
'h' => '',
'H' => '',
'i' => '',
's' => '',
'u' => '',
'e' => '', // deprecated since version 1.6.0 of moment.js
'I' => '', // no equivalent
'O' => '', // no equivalent
'P' => '', // no equivalent
'T' => '', // no equivalent
'Z' => '', // no equivalent
'c' => '', // no equivalent
'r' => '', // no equivalent
'U' => '',
'-' => '-',
'/' => '/',
'.' => '.',
':' => ''
];
$timeOnly = strtr($format, $replacements);
return trim($timeOnly);
}
/**
* @inheritdoc
*/
public static function convertPhpToMomentFormat($format)
{
$replacements = [
'd' => 'DD',
'D' => 'ddd',
'j' => 'D',
'l' => 'dddd',
'N' => 'E',
'S' => 'o',
'w' => 'e',
'z' => 'DDD',
'W' => 'W',
'F' => 'MMMM',
'm' => 'MM',
'M' => 'MMM',
'n' => 'M',
't' => '', // no equivalent
'L' => '', // no equivalent
'o' => 'YYYY',
'Y' => 'YYYY',
'y' => 'YY',
'a' => 'a',
'A' => 'A',
'B' => '', // no equivalent
'g' => 'h',
'G' => 'H',
'h' => 'hh',
'H' => 'HH',
'i' => 'mm',
's' => 'ss',
'u' => 'SSS',
'e' => 'zz', // deprecated since version 1.6.0 of moment.js
'I' => '', // no equivalent
'O' => '', // no equivalent
'P' => '', // no equivalent
'T' => '', // no equivalent
'Z' => '', // no equivalent
'c' => '', // no equivalent
'r' => '', // no equivalent
'U' => 'X',
];
$momentFormat = strtr($format, $replacements);
return $momentFormat;
}
/**
* @inheritdoc
*/
public static function convertMomentToJalaliFormat($format)
{
$replacements = [
'DD' => 'jDD',
'ddd' => 'ddd',
'D' => 'jD',
'dddd' => 'dddd',
'E' => 'E',
'e' => 'e',
'DDD' => 'jDDD',
'W' => '',
'MMMM' => 'jMMMM',
'MM' => 'jMM',
'MMM' => 'jMMM',
'M' => 'jM',
'YYYY' => 'jYYYY',
'YY' => 'jYY',
'a' => 'a',
'A' => 'A',
'h' => 'h',
'H' => 'H',
'hh' => 'hh',
'HH' => 'HH',
'mm' => 'mm',
'ss' => 'ss',
'SSS' => 'SSS',
'X' => 'X'
];
$timeOnly = strtr($format, $replacements);
return trim($timeOnly);
}
/**
* Timezone identifiers
* @return array
*/
public static function timezoneList()
{
if (self::$timezones === null) {
self::$timezones = [];
$offsets = [];
$now = new Carbon('now');
foreach (\DateTimeZone::listIdentifiers() as $timezone) {
$now->setTimezone(new \DateTimeZone($timezone));
$offsets[] = $offset = $now->getOffset();
self::$timezones[$timezone] = '(' . self::formatGmtOffset($offset) . ') ' . self::formatTimezoneName($timezone);
}
array_multisort($offsets, self::$timezones);
}
return self::$timezones;
}
private static function formatGmtOffset($offset) {
$hours = intval($offset / 3600);
$minutes = abs(intval($offset % 3600 / 60));
return 'GMT' . ($offset ? sprintf('%+03d:%02d', $hours, $minutes) : '');
}
private static function formatTimezoneName($name) {
$name = str_replace('/', ', ', $name);
$name = str_replace('_', ' ', $name);
$name = str_replace('St ', 'St. ', $name);
return $name;
}
}

356
lib/Helper/Environment.php Normal file
View File

@@ -0,0 +1,356 @@
<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use Phinx\Console\PhinxApplication;
use Phinx\Wrapper\TextWrapper;
/**
* Class Environment
* @package Xibo\Helper
*/
class Environment
{
public static $WEBSITE_VERSION_NAME = '4.4.0-alpha2';
public static $XMDS_VERSION = '7';
public static $XLF_VERSION = 4;
public static $VERSION_REQUIRED = '8.1.0';
public static $VERSION_UNSUPPORTED = '9.0';
public static $PLAYER_SUPPORT = 300;
/** @var null cache migration status for the whole request */
private static $migrationStatus = null;
/** @var string the git commit ref */
private static $gitCommit = null;
/**
* Is there a migration pending?
* @return bool
*/
public static function migrationPending()
{
// Allow missing migrations in dev mode.
if (self::isDevMode()) {
return self::getMigrationStatus() > 2;
} else {
return self::getMigrationStatus() != 0;
}
}
/**
* Get Migration Status
* @return int
*/
private static function getMigrationStatus()
{
if (self::$migrationStatus === null) {
// Use a Phinx text wrapper to work out what the current status is
// make sure this does not output anything to our output buffer
ob_start();
$phinx = new TextWrapper(new PhinxApplication(), ['configuration' => PROJECT_ROOT . '/phinx.php']);
$phinx->getStatus();
self::$migrationStatus = $phinx->getExitCode();
ob_end_clean();
}
return self::$migrationStatus;
}
/**
* Get Git Commit
* @return string
*/
public static function getGitCommit()
{
if (self::$gitCommit === null) {
if (isset($_SERVER['GIT_COMMIT']) && $_SERVER['GIT_COMMIT'] === 'dev') {
$out = [];
exec('cat /var/www/cms/.git/$(cat /var/www/cms/.git/HEAD | cut -d\' \' -f2)', $out);
self::$gitCommit = $out[0] ?? null;
} else {
self::$gitCommit = $_SERVER['GIT_COMMIT'] ?? null;
}
if (self::$gitCommit === null && file_exists(PROJECT_ROOT . '/commit.sha')) {
self::$gitCommit = trim(file_get_contents(PROJECT_ROOT . '/commit.sha'));
}
}
return self::$gitCommit ?? 'unknown';
}
/**
* Check FileSystem Permissions
* @return bool
*/
public static function checkSettingsFileSystemPermissions()
{
$settingsPath = PROJECT_ROOT . '/web/settings.php';
return (file_exists($settingsPath)) ? is_writable($settingsPath) : is_writable(PROJECT_ROOT . '/web');
}
/**
* Check FileSystem Permissions
* @return bool
*/
public static function checkCacheFileSystemPermissions()
{
return is_writable(PROJECT_ROOT . '/cache');
}
/**
* Check PHP version is within the preset parameters
* @return bool
*/
public static function checkPHP()
{
return (version_compare(phpversion(), self::$VERSION_REQUIRED) != -1) && (version_compare(phpversion(), self::$VERSION_UNSUPPORTED) != 1);
}
/**
* Check PHP has the PDO module installed (with MySQL driver)
*/
public static function checkPDO()
{
return extension_loaded("pdo_mysql");
}
/**
* Check PHP has the GetText module installed
* @return bool
*/
public static function checkGettext()
{
return extension_loaded("gettext");
}
/**
* Check PHP has JSON module installed
* @return bool
*/
public static function checkJson()
{
return extension_loaded("json");
}
/**
*
* Check PHP has SOAP module installed
* @return bool
*/
public static function checkSoap()
{
return extension_loaded("soap");
}
/**
* Check PHP has GD module installed
* @return bool
*/
public static function checkGd()
{
return extension_loaded("gd");
}
/**
* Check PHP has the DOM XML functionality installed
* @return bool
*/
public static function checkDomXml()
{
return extension_loaded("dom");
}
/**
* Check PHP has the DOM functionality installed
* @return bool
*/
public static function checkDom()
{
return class_exists("DOMDocument");
}
/**
* Check PHP has session functionality installed
* @return bool
*/
public static function checkSession()
{
return extension_loaded("session");
}
/**
* Check PHP has PCRE functionality installed
* @return bool
*/
public static function checkPCRE()
{
return extension_loaded("pcre");
}
/**
* Check PHP has FileInfo functionality installed
* @return bool
*/
public static function checkFileInfo()
{
return extension_loaded("fileinfo");
}
public static function checkZip()
{
return extension_loaded('zip');
}
public static function checkIntlDateFormat()
{
return class_exists('IntlDateFormatter');
}
/**
* Check to see if curl is installed
*/
public static function checkCurlInstalled()
{
return function_exists('curl_version');
}
/**
* Check PHP is setup for large file uploads
* @return bool
*/
public static function checkPHPUploads()
{
# Consider 0 - 128M warning / < 120 seconds
# Variables to check:
# post_max_size
# upload_max_filesize
# max_execution_time
$minSize = ByteFormatter::toBytes('128M');
if (ByteFormatter::toBytes(ini_get('post_max_size')) < $minSize)
return false;
if (ByteFormatter::toBytes(ini_get('upload_max_filesize')) < $minSize)
return false;
if (ini_get('max_execution_time') < 120)
return false;
// All passed
return true;
}
public static function getMaxUploadSize()
{
return ini_get('upload_max_filesize');
}
/**
* Check open ssl is available
* @return bool
*/
public static function checkOpenSsl()
{
return extension_loaded('openssl');
}
/**
* @inheritdoc
* https://stackoverflow.com/a/45767760
*/
public static function getMemoryLimitBytes()
{
return intval(str_replace(array('G', 'M', 'K'), array('000000000', '000000', '000'), ini_get('memory_limit')));
}
/**
* @return bool
*/
public static function checkTimezoneIdentifiers()
{
return function_exists('timezone_identifiers_list');
}
/**
* @return bool
*/
public static function checkAllowUrlFopen()
{
return ini_get('allow_url_fopen');
}
/**
* @return bool
*/
public static function checkCurl()
{
return extension_loaded('curl');
}
/**
* @return bool
*/
public static function checkSimpleXml()
{
return extension_loaded('simplexml');
}
/**
* @return bool
*/
public static function checkGnu()
{
return extension_loaded('gnupg');
}
/**
* @param $url
* @return bool
*/
public static function checkUrl($url)
{
return (stripos($url, '/web/') === false);
}
/**
* Is the CMS in DEV mode?
* @return bool
*/
public static function isDevMode()
{
return (isset($_SERVER['CMS_DEV_MODE']) && $_SERVER['CMS_DEV_MODE'] === 'true');
}
/**
* Is debugging forced ON for this request?
* @return bool
*/
public static function isForceDebugging()
{
return (isset($_SERVER['CMS_FORCE_DEBUG']) && $_SERVER['CMS_FORCE_DEBUG'] === 'true');
}
}

View File

@@ -0,0 +1,122 @@
<?php
/**
* Copyright (c) 2012-2015 Josh Lockhart
*/
namespace Xibo\Helper;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
/**
* A set of helper methods for dealing with HTTP cache
*/
class HttpCacheProvider
{
/**
* Enable client-side HTTP caching
*
* @param ResponseInterface $response PSR7 response object
* @param string $type Cache-Control type: "private" or "public"
* @param null|int|string $maxAge Maximum cache age (integer timestamp or datetime string)
* @param bool $mustRevalidate add option "must-revalidate" to Cache-Control
*
* @return ResponseInterface A new PSR7 response object with `Cache-Control` header
* @throws InvalidArgumentException if the cache-control type is invalid
*/
public static function allowCache(ResponseInterface $response, $type = 'private', $maxAge = null, $mustRevalidate = false)
{
if (!in_array($type, ['private', 'public'])) {
throw new InvalidArgumentException('Invalid Cache-Control type. Must be "public" or "private".');
}
$headerValue = $type;
if ($maxAge || is_integer($maxAge)) {
if (!is_integer($maxAge)) {
$maxAge = strtotime($maxAge);
}
$headerValue = $headerValue . ', max-age=' . $maxAge;
}
if ($mustRevalidate) {
$headerValue = $headerValue . ", must-revalidate";
}
return $response->withHeader('Cache-Control', $headerValue);
}
/**
* Disable client-side HTTP caching
*
* @param ResponseInterface $response PSR7 response object
*
* @return ResponseInterface A new PSR7 response object with `Cache-Control` header
*/
public static function denyCache(ResponseInterface $response)
{
return $response->withHeader('Cache-Control', 'no-store,no-cache');
}
/**
* Add `Expires` header to PSR7 response object
*
* @param ResponseInterface $response A PSR7 response object
* @param int|string $time A UNIX timestamp or a valid `strtotime()` string
*
* @return ResponseInterface A new PSR7 response object with `Expires` header
* @throws InvalidArgumentException if the expiration date cannot be parsed
*/
public static function withExpires(ResponseInterface $response, $time)
{
if (!is_integer($time)) {
$time = strtotime($time);
if ($time === false) {
throw new InvalidArgumentException('Expiration value could not be parsed with `strtotime()`.');
}
}
return $response->withHeader('Expires', gmdate('D, d M Y H:i:s T', $time));
}
/**
* Add `ETag` header to PSR7 response object
*
* @param ResponseInterface $response A PSR7 response object
* @param string $value The ETag value
* @param string $type ETag type: "strong" or "weak"
*
* @return ResponseInterface A new PSR7 response object with `ETag` header
* @throws InvalidArgumentException if the etag type is invalid
*/
public static function withEtag(ResponseInterface $response, $value, $type = 'strong')
{
if (!in_array($type, ['strong', 'weak'])) {
throw new InvalidArgumentException('Invalid etag type. Must be "strong" or "weak".');
}
$value = '"' . $value . '"';
if ($type === 'weak') {
$value = 'W/' . $value;
}
return $response->withHeader('ETag', $value);
}
/**
* Add `Last-Modified` header to PSR7 response object
*
* @param ResponseInterface $response A PSR7 response object
* @param int|string $time A UNIX timestamp or a valid `strtotime()` string
*
* @return ResponseInterface A new PSR7 response object with `Last-Modified` header
* @throws InvalidArgumentException if the last modified date cannot be parsed
*/
public static function withLastModified(ResponseInterface $response, $time)
{
if (!is_integer($time)) {
$time = strtotime($time);
if ($time === false) {
throw new InvalidArgumentException('Last Modified value could not be parsed with `strtotime()`.');
}
}
return $response->withHeader('Last-Modified', gmdate('D, d M Y H:i:s T', $time));
}
}

205
lib/Helper/HttpsDetect.php Normal file
View File

@@ -0,0 +1,205 @@
<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use Slim\Http\ServerRequest;
/**
* Class HttpsDetect
* @package Xibo\Helper
*/
class HttpsDetect
{
/**
* Get the root of the web server
* this should only be used if you're planning to append the path
* @return string
*/
public function getRootUrl(): string
{
$url = $this->getScheme() . '://' . $this->getHost();
if (($this->getScheme() === 'https' && $this->getPort() !== 443)
|| ($this->getScheme() === 'http' && $this->getPort() !== 80)
) {
$url .= sprintf(':%s', $this->getPort());
}
return $url;
}
/**
* @deprecated use getRootUrl
* @return string
*/
public function getUrl(): string
{
return $this->getRootUrl();
}
/**
* Get the base URL for the instance
* this should give us the CMS URL including alias and file
* @param \Slim\Http\ServerRequest|null $request
* @return string
*/
public function getBaseUrl(?ServerRequest $request = null): string
{
// Check REQUEST_URI is set. IIS doesn't set it, so we need to build it
// Attribution:
// Code snippet from http://support.ecenica.com/web-hosting/scripting/troubleshooting-scripting-errors/how-to-fix-server-request_uri-php-error-on-windows-iis/
// Released under BSD License
// Copyright (c) 2009, Ecenica Limited All rights reserved.
if (!isset($_SERVER['REQUEST_URI'])) {
$_SERVER['REQUEST_URI'] = $_SERVER['PHP_SELF'];
if (isset($_SERVER['QUERY_STRING'])) {
$_SERVER['REQUEST_URI'] .= '?' . $_SERVER['QUERY_STRING'];
}
}
// End Code Snippet
// The request URL should be everything after the host, i.e:
// /xmds.php?file=
// /xibo/xmds.php?file=
// /playersoftware
// /xibo/playersoftware
$requestUri = explode('?', htmlentities($_SERVER['REQUEST_URI'], ENT_QUOTES, 'UTF-8'));
$baseUrl = $this->getRootUrl() . '/' . ltrim($requestUri[0], '/');
// We use the path, if provided, to remove any known path information
// i.e. if we're running in a sub-folder we might be on /xibo/playersoftware
// in which case we want to remove /playersoftware to get to /xibo which is the base path.
$path = $request?->getUri()?->getPath() ?? '';
if (!empty($path)) {
$baseUrl = str_replace($path, '', $baseUrl);
}
return $baseUrl;
}
/**
* @return string
*/
public function getScheme(): string
{
return ($this->isHttps()) ? 'https' : 'http';
}
/**
* Get Host
* @return string
*/
public function getHost(): string
{
if (isset($_SERVER['HTTP_HOST'])) {
$httpHost = htmlentities($_SERVER['HTTP_HOST'], ENT_QUOTES, 'UTF-8');
if (str_contains($httpHost, ':')) {
$hostParts = explode(':', $httpHost);
return $hostParts[0];
}
return $httpHost;
}
return $_SERVER['SERVER_NAME'];
}
/**
* Get Port
* @return int
*/
public function getPort(): int
{
if (isset($_SERVER['HTTP_HOST']) && str_contains($_SERVER['HTTP_HOST'], ':')) {
$hostParts = explode(':', htmlentities($_SERVER['HTTP_HOST'], ENT_QUOTES, 'UTF-8'));
return $hostParts[1];
}
return ($this->isHttps() ? 443 : 80);
}
/**
* Is HTTPs?
* @return bool
*/
public static function isHttps(): bool
{
return (
(isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on') ||
(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https')
);
}
/**
* @param \Xibo\Service\ConfigServiceInterface $config
* @param \Psr\Http\Message\RequestInterface $request
* @return bool
*/
public static function isShouldIssueSts($config, $request): bool
{
// We might need to issue STS headers
$whiteListLoadBalancers = $config->getSetting('WHITELIST_LOAD_BALANCERS');
$originIp = $_SERVER['REMOTE_ADDR'] ?? '';
$forwardedProtoHttps = (
strtolower($request->getHeaderLine('HTTP_X_FORWARDED_PROTO')) === 'https'
&& $originIp != ''
&& (
$whiteListLoadBalancers === '' || in_array($originIp, explode(',', $whiteListLoadBalancers))
)
);
return (
($request->getUri()->getScheme() == 'https' || $forwardedProtoHttps)
&& $config->getSetting('ISSUE_STS', 0) == 1
);
}
/**
* @param \Xibo\Service\ConfigServiceInterface $config
* @param \Psr\Http\Message\ResponseInterface $response
* @return \Psr\Http\Message\ResponseInterface
*/
public static function decorateWithSts($config, $response)
{
return $response->withHeader(
'strict-transport-security',
'max-age=' . $config->getSetting('STS_TTL', 600)
);
}
/**
* @param \Xibo\Service\ConfigServiceInterface $config
* @param \Psr\Http\Message\RequestInterface $request
* @param \Psr\Http\Message\ResponseInterface $response
* @return \Psr\Http\Message\ResponseInterface
*/
public static function decorateWithStsIfNecessary($config, $request, $response)
{
if (self::isShouldIssueSts($config, $request)) {
return self::decorateWithSts($config, $response);
} else {
return $response;
}
}
}

576
lib/Helper/Install.php Normal file
View File

@@ -0,0 +1,576 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use Phinx\Console\PhinxApplication;
use Phinx\Wrapper\TextWrapper;
use Psr\Container\ContainerInterface;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InstallationError;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* Class Install
* @package Xibo\Helper
*/
class Install
{
// DB Details
public $db_create;
public $db_admin_user;
public $db_admin_pass;
public $new_db_host;
public $new_db_user;
public $new_db_pass;
public $new_db_name;
public $new_ssl_ca;
public $new_ssl_verify;
public $existing_db_host;
public $existing_db_user;
public $existing_db_pass;
public $existing_db_name;
public $existing_ssl_ca;
public $existing_ssl_verify;
/** @var ContainerInterface */
private $container;
/** @var SanitizerService */
private $sanitizerService;
/**
* Install constructor.
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->sanitizerService = $container->get('sanitizerService');
}
/**
* @param $array
* @return SanitizerInterface
*/
protected function getSanitizer($array)
{
return $this->sanitizerService->getSanitizer($array);
}
/**
* @return array
*/
public function step1(): array
{
return [
'config' => $this->container->get('configService'),
'isSettingsPathWriteable' => Environment::checkSettingsFileSystemPermissions()
];
}
/**
* @return array
*/
public function step2(): array
{
return [];
}
/**
* @param Request $request
* @param Response $response
* @throws InstallationError
*/
public function step3(Request $request, Response $response) : Response
{
$sanitizedParams = $this->getSanitizer($request->getParams());
/** @var StorageServiceInterface $store */
$store = $this->container->get('store');
// Have we been told to create a new database
$this->db_create = $sanitizedParams->getInt('db_create');
// Check all parameters have been specified
$this->db_admin_user = $sanitizedParams->getString('admin_username');
$this->db_admin_pass = $sanitizedParams->getString('admin_password');
$this->new_db_host = $sanitizedParams->getString('host');
$this->new_db_user = $sanitizedParams->getString('db_username');
$this->new_db_pass = $sanitizedParams->getString('db_password');
$this->new_db_name = $sanitizedParams->getString('db_name');
$this->new_ssl_ca = $sanitizedParams->getString('ssl_ca');
$this->new_ssl_verify = $sanitizedParams->getCheckbox('ssl_verify') == 1;
$this->existing_db_host = $sanitizedParams->getString('existing_host');
$this->existing_db_user = $sanitizedParams->getString('existing_db_username');
$this->existing_db_pass = $sanitizedParams->getString('existing_db_password');
$this->existing_db_name = $sanitizedParams->getString('existing_db_name');
$this->existing_ssl_ca = $sanitizedParams->getString('existing_ssl_ca');
$this->existing_ssl_verify = $sanitizedParams->getCheckbox('existing_ssl_verify') == 1;
// If an administrator user name / password has been specified then we should create a new DB
if ($this->db_create == 1) {
// Check details for a new database
if ($this->new_db_host == '') {
throw new InstallationError(__('Please provide a database host. This is usually localhost.'));
}
if ($this->new_db_user == '') {
throw new InstallationError(__('Please provide a user for the new database.'));
}
if ($this->new_db_pass == '') {
throw new InstallationError(__('Please provide a password for the new database.'));
}
if ($this->new_db_name == '') {
throw new InstallationError(__('Please provide a name for the new database.'));
}
if ($this->db_admin_user == '') {
throw new InstallationError(__('Please provide an admin user name.'));
}
// Try to create the new database
// Try and connect using these details and create the new database
try {
$store->connect(
$this->new_db_host,
$this->db_admin_user,
$this->db_admin_pass,
null,
empty($this->new_ssl_ca) ? null : $this->new_ssl_ca,
$this->new_ssl_verify
);
} catch (\PDOException $e) {
throw new InstallationError(sprintf(
__('Could not connect to MySQL with the administrator details. Please check and try again. Error Message = [%s]'),
$e->getMessage()
));
}
// Try to create the new database
try {
$dbh = $store->getConnection();
$dbh->exec(sprintf('CREATE DATABASE `%s` CHARACTER SET utf8 COLLATE utf8_general_ci', $this->new_db_name));
} catch (\PDOException $e) {
throw new InstallationError(sprintf(__('Could not create a new database with the administrator details [%s]. Please check and try again. Error Message = [%s]'), $this->db_admin_user, $e->getMessage()));
}
// Try to create the new user
$sql = null;
try {
$dbh = $store->getConnection();
// Create the user and grant privileges
if ($this->new_db_host == 'localhost') {
$sql = sprintf(
'GRANT ALL PRIVILEGES ON `%s`.* to %s@%s IDENTIFIED BY %s',
$this->new_db_name,
$dbh->quote($this->new_db_user),
$dbh->quote($this->new_db_host),
$dbh->quote($this->new_db_pass)
);
} else {
$sql = sprintf(
'GRANT ALL PRIVILEGES ON `%s`.* to %s@\'%%\' IDENTIFIED BY %s',
$this->new_db_name,
$dbh->quote($this->new_db_user),
$dbh->quote($this->new_db_pass)
);
}
$dbh->exec($sql);
// Flush
$dbh->exec('FLUSH PRIVILEGES');
} catch (\PDOException $e) {
throw new InstallationError(sprintf(
__('Could not create a new user with the administrator details. Please check and try again. Error Message = [%s]. SQL = [%s].'),
$e->getMessage(),
$sql
));
}
// Set our DB details
$this->existing_db_host = $this->new_db_host;
$this->existing_db_user = $this->new_db_user;
$this->existing_db_pass = $this->new_db_pass;
$this->existing_db_name = $this->new_db_name;
$this->existing_ssl_ca = $this->new_ssl_ca;
$this->existing_ssl_verify = $this->new_ssl_verify;
// Close the connection
$store->close();
} else {
// Check details for a new database
if ($this->existing_db_host == '') {
throw new InstallationError(__('Please provide a database host. This is usually localhost.'));
}
if ($this->existing_db_user == '') {
throw new InstallationError(__('Please provide a user for the existing database.'));
}
if ($this->existing_db_pass == '') {
throw new InstallationError(__('Please provide a password for the existing database.'));
}
if ($this->existing_db_name == '') {
throw new InstallationError(__('Please provide a name for the existing database.'));
}
}
// Try and make a connection with this database
try {
$store->connect(
$this->existing_db_host,
$this->existing_db_user,
$this->existing_db_pass,
$this->existing_db_name,
empty($this->existing_ssl_ca) ? null : $this->existing_ssl_ca,
$this->existing_ssl_verify
);
} catch (\PDOException $e) {
throw new InstallationError(sprintf(
__('Could not connect to MySQL with the administrator details. Please check and try again. Error Message = [%s]'),
$e->getMessage()
));
}
// Write out a new settings.php
$fh = fopen(PROJECT_ROOT . '/web/settings.php', 'wt');
if (!$fh) {
throw new InstallationError(
__('Unable to write to settings.php. We already checked this was possible earlier, so something changed.')
);
}
// Get the settings template and issue replacements
$settings = $this->getSettingsTemplate();
// Replace instances of $_SERVER vars with our own
$settings = str_replace('$_SERVER[\'MYSQL_HOST\'] . \':\' . $_SERVER[\'MYSQL_PORT\']', '\'' . $this->existing_db_host . '\'', $settings);
$settings = str_replace('$_SERVER[\'MYSQL_USER\']', '\'' . $this->existing_db_user . '\'', $settings);
$settings = str_replace('$_SERVER[\'MYSQL_PASSWORD\']', '\'' . addslashes($this->existing_db_pass) . '\'', $settings);
$settings = str_replace('$_SERVER[\'MYSQL_DATABASE\']', '\'' . $this->existing_db_name . '\'', $settings);
$settings = str_replace('$_SERVER[\'MYSQL_ATTR_SSL_CA\']', '\'' . $this->existing_ssl_ca . '\'', $settings);
$settings = str_replace('$_SERVER[\'MYSQL_ATTR_SSL_VERIFY_SERVER_CERT\']', '\'' . $this->existing_ssl_verify . '\'', $settings);
$settings = str_replace('define(\'SECRET_KEY\',\'\')', 'define(\'SECRET_KEY\',\'' . Install::generateSecret() . '\');', $settings);
if (!fwrite($fh, $settings)) {
throw new InstallationError(__('Unable to write to settings.php. We already checked this was possible earlier, so something changed.'));
}
fclose($fh);
// Run phinx migrate
$phinx = new TextWrapper(new PhinxApplication(), ['configuration' => PROJECT_ROOT . '/phinx.php']);
$phinx->getMigrate();
// If we get here, we want to move on to the next step.
// This is handled by the calling function (i.e. there is no output from this call, we just reload and move on)
return $response;
}
/**
* @return array
*/
public function step4(): array
{
return [];
}
/**
* @param Request $request
* @param Response $response
* @throws InstallationError
*/
public function step5(Request $request, Response $response) : Response
{
$sanitizedParams = $this->getSanitizer($request->getParams());
/** @var StorageServiceInterface $store */
$store = $this->container->get('store');
// Configure the user account
$username = $sanitizedParams->getString('admin_username');
$password = $sanitizedParams->getString('admin_password');
if ($username == '') {
throw new InstallationError(__('Missing the admin username.'));
}
if ($password == '') {
throw new InstallationError(__('Missing the admin password.'));
}
// Update user id 1 with these details.
try {
$dbh = $store->getConnection();
$sth = $dbh->prepare('UPDATE `user` SET UserName = :username, UserPassword = :password WHERE UserID = 1 LIMIT 1');
$sth->execute(array(
'username' => $username,
'password' => md5($password)
));
// Update group ID 3 with the user name
$sth = $dbh->prepare('UPDATE `group` SET `group` = :username WHERE groupId = 3 LIMIT 1');
$sth->execute(array(
'username' => $username
));
} catch (\PDOException $e) {
throw new InstallationError(sprintf(__('Unable to set the user details. This is an unexpected error, please contact support. Error Message = [%s]'), $e->getMessage()));
}
return $response;
}
/**
* @return array
*/
public function step6(): array
{
return [
'serverKey' => Install::generateSecret(6)
];
}
/**
* @param Request $request
* @param Response $response
* @throws InstallationError
*/
public function step7(Request $request, Response $response) : Response
{
$sanitizedParams = $this->getSanitizer($request->getParams());
/** @var StorageServiceInterface $store */
$store = $this->container->get('store');
$server_key = $sanitizedParams->getString('server_key');
$library_location = $sanitizedParams->getString('library_location');
$stats = $sanitizedParams->getCheckbox('stats');
if ($server_key == '') {
throw new InstallationError(__('Missing the server key.'));
}
if ($library_location == '') {
throw new InstallationError(__('Missing the library location.'));
}
// Remove trailing white space from the path given.
$library_location = trim($library_location);
if (!is_dir($library_location)) {
// Make sure they haven't given a file as the library location
if (is_file($library_location)) {
throw new InstallationError(__('A file exists with the name you gave for the Library Location. Please choose another location'));
}
// Directory does not exist. Attempt to make it
// Using mkdir recursively, so it will attempt to make any
// intermediate folders required.
if (!mkdir($library_location, 0755, true)) {
throw new InstallationError(__('Could not create the Library Location directory for you. Please ensure the webserver has permission to create a folder in this location, or create the folder manually and grant permission for the webserver to write to the folder.'));
}
}
// Is library_location writable?
if (!is_writable($library_location)) {
throw new InstallationError(__('The Library Location you gave is not writable by the webserver. Please fix the permissions and try again.'));
}
// Is library_location empty?
if (count(Install::ls("*", $library_location, true)) > 0) {
throw new InstallationError(__('The Library Location you gave is not empty. Please give the location of an empty folder'));
}
// Check if the user has added a trailing slash. If not, add one.
if (!((substr($library_location, -1) == '/') || (substr($library_location, -1) == '\\'))) {
$library_location = $library_location . '/';
}
// Attempt to create fonts sub-folder in Library location
if (!mkdir($library_location . 'fonts', 0777, true)) {
throw new InstallationError(__('Could not create the fonts sub-folder under Library Location directory for you. Please ensure the webserver has permission to create a folder in this location, or create the folder manually and grant permission for the webserver to write to the folder.'));//phpcs:ignore
}
try {
$dbh = $store->getConnection();
// Library Location
$sth = $dbh->prepare('UPDATE `setting` SET `value` = :value WHERE `setting`.`setting` = \'LIBRARY_LOCATION\' LIMIT 1');
$sth->execute(array('value' => $library_location));
// Server Key
$sth = $dbh->prepare('UPDATE `setting` SET `value` = :value WHERE `setting`.`setting` = \'SERVER_KEY\' LIMIT 1');
$sth->execute(array('value' => $server_key));
// Default Time zone
$sth = $dbh->prepare('UPDATE `setting` SET `value` = :value WHERE `setting`.`setting` = \'defaultTimezone\' LIMIT 1');
$sth->execute(array('value' => date_default_timezone_get()));
// Phone Home
$sth = $dbh->prepare('UPDATE `setting` SET `value` = :value WHERE `setting`.`setting` = \'PHONE_HOME\' LIMIT 1');
$sth->execute([
'value' => $stats
]);
} catch (\PDOException $e) {
throw new InstallationError(sprintf(__('An error occurred updating these settings. This is an unexpected error, please contact support. Error Message = [%s]'), $e->getMessage()));
}
// Delete install
if (!@unlink('index.php')) {
throw new InstallationError(__("Unable to delete install/index.php. Please ensure the web server has permission to unlink this file and retry"));
}
return $response;
}
/**
* This function will take a pattern and a folder as the argument and go thru it(recursively if needed)and return the list of
* all files in that folder.
* Link : http://www.bin-co.com/php/scripts/filesystem/ls/
* License : BSD
* Arguments : $pattern - The pattern to look out for [OPTIONAL]
* $folder - The path of the directory of which's directory list you want [OPTIONAL]
* $recursivly - The funtion will traverse the folder tree recursivly if this is true. Defaults to false. [OPTIONAL]
* $options - An array of values 'return_files' or 'return_folders' or both
* Returns : A flat list with the path of all the files(no folders) that matches the condition given.
*/
public static function ls($pattern = '*', $folder = '', $recursivly = false, $options = ['return_files', 'return_folders']): array
{
if ($folder) {
$current_folder = realpath('.');
if (in_array('quiet', $options)) { // If quiet is on, we will suppress the 'no such folder' error
if (!file_exists($folder)) return array();
}
if (!chdir($folder)) return array();
}
$get_files = in_array('return_files', $options);
$get_folders = in_array('return_folders', $options);
$both = array();
$folders = array();
// Get the all files and folders in the given directory.
if ($get_files) $both = glob($pattern, GLOB_BRACE + GLOB_MARK);
if ($recursivly or $get_folders) $folders = glob("*", GLOB_ONLYDIR + GLOB_MARK);
//If a pattern is specified, make sure even the folders match that pattern.
$matching_folders = array();
if ($pattern !== '*') $matching_folders = glob($pattern, GLOB_ONLYDIR + GLOB_MARK);
//Get just the files by removing the folders from the list of all files.
$all = array_values(array_diff($both, $folders));
if ($recursivly or $get_folders) {
foreach ($folders as $this_folder) {
if ($get_folders) {
//If a pattern is specified, make sure even the folders match that pattern.
if ($pattern !== '*') {
if (in_array($this_folder, $matching_folders)) array_push($all, $this_folder);
} else array_push($all, $this_folder);
}
if ($recursivly) {
// Continue calling this function for all the folders
$deep_items = Install::ls($pattern, $this_folder, $recursivly, $options); # :RECURSION:
foreach ($deep_items as $item) {
array_push($all, $this_folder . $item);
}
}
}
}
if ($folder) chdir($current_folder);
return $all;
}
/**
* @param int $length
* @return string
*/
public static function generateSecret($length = 12): string
{
# Generates a random 12 character alphanumeric string to use as a salt
mt_srand((double)microtime() * 1000000);
$key = "";
for ($i = 0; $i < $length; $i++) {
$c = mt_rand(0, 2);
if ($c == 0) {
$key .= chr(mt_rand(65, 90));
} elseif ($c == 1) {
$key .= chr(mt_rand(97, 122));
} else {
$key .= chr(mt_rand(48, 57));
}
}
return $key;
}
private function getSettingsTemplate()
{
return <<<END
<?php
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo - and is automatically generated by the installer
*
* You should not need to edit this file, unless your SQL connection details have changed.
*/
defined('XIBO') or die(__("Sorry, you are not allowed to directly access this page.") . "<br />" . __("Please press the back button in your browser."));
global \$dbhost;
global \$dbuser;
global \$dbpass;
global \$dbname;
global \$dbssl;
global \$dbsslverify;
\$dbhost = \$_SERVER['MYSQL_HOST'] . ':' . \$_SERVER['MYSQL_PORT'];
\$dbuser = \$_SERVER['MYSQL_USER'];
\$dbpass = \$_SERVER['MYSQL_PASSWORD'];
\$dbname = \$_SERVER['MYSQL_DATABASE'];
\$dbssl = \$_SERVER['MYSQL_ATTR_SSL_CA'];
\$dbsslverify = \$_SERVER['MYSQL_ATTR_SSL_VERIFY_SERVER_CERT'];
if (!defined('SECRET_KEY'))
define('SECRET_KEY','');
if (file_exists('/var/www/cms/custom/settings-custom.php'))
include_once('/var/www/cms/custom/settings-custom.php');
END;
}
}

View File

@@ -0,0 +1,143 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use Exception;
use Xibo\Support\Exception\LibraryFullException;
/**
* Class LayoutUploadHandler
* @package Xibo\Helper
*/
class LayoutUploadHandler extends BlueImpUploadHandler
{
/**
* @param $file
* @param $index
*/
protected function handleFormData($file, $index)
{
/* @var \Xibo\Controller\Layout $controller */
$controller = $this->options['controller'];
/* @var SanitizerService $sanitizerService */
$sanitizerService = $this->options['sanitizerService'];
// Handle form data, e.g. $_REQUEST['description'][$index]
$fileName = $file->name;
$controller->getLog()->debug('Upload complete for ' . $fileName . '.');
// Upload and Save
try {
// Check Library
if ($this->options['libraryQuotaFull']) {
throw new LibraryFullException(sprintf(
__('Your library is full. Library Limit: %s K'),
$this->options['libraryLimit']
));
}
// Check for a user quota
$controller->getUser()->isQuotaFullByUser();
$params = $sanitizerService->getSanitizer($_REQUEST);
// Parse parameters
$name = htmlspecialchars($params->getArray('name')[$index]);
$tags = $controller->getUser()->featureEnabled('tag.tagging')
? htmlspecialchars($params->getArray('tags')[$index])
: '';
$template = $params->getCheckbox('template', ['default' => 0]);
$replaceExisting = $params->getCheckbox('replaceExisting', ['default' => 0]);
$importTags = $params->getCheckbox('importTags', ['default' => 0]);
$useExistingDataSets = $params->getCheckbox('useExistingDataSets', ['default' => 0]);
$importDataSetData = $params->getCheckbox('importDataSetData', ['default' => 0]);
$importFallback = $params->getCheckbox('importFallback', ['default' => 0]);
$layout = $controller->getLayoutFactory()->createFromZip(
$controller->getConfig()->getSetting('LIBRARY_LOCATION') . 'temp/' . $fileName,
$name,
$this->options['userId'],
$template,
$replaceExisting,
$importTags,
$useExistingDataSets,
$importDataSetData,
$this->options['dataSetFactory'],
$tags,
$this->options['mediaService'],
$this->options['folderId'],
);
// set folderId, permissionFolderId is handled on Layout specific Campaign record.
$layout->folderId = $this->options['folderId'];
$layout->save(['saveActions' => false, 'import' => true]);
if (!empty($layout->getUnmatchedProperty('thumbnail'))) {
rename($layout->getUnmatchedProperty('thumbnail'), $layout->getThumbnailUri());
}
$layout->managePlaylistClosureTable();
// When importing a layout, skip action validation
$layout->manageActions(false);
// Handle widget data
$fallback = $layout->getUnmatchedProperty('fallback');
if ($importFallback == 1 && $fallback !== null) {
/** @var \Xibo\Factory\WidgetDataFactory $widgetDataFactory */
$widgetDataFactory = $this->options['widgetDataFactory'];
foreach ($layout->getAllWidgets() as $widget) {
// Did this widget have fallback data included in its export?
if (array_key_exists($widget->tempWidgetId, $fallback)) {
foreach ($fallback[$widget->tempWidgetId] as $item) {
// We create the widget data with the new widgetId
$widgetDataFactory
->create(
$widget->widgetId,
$item['data'] ?? [],
intval($item['displayOrder'] ?? 1),
)
->save();
}
}
}
}
@unlink($controller->getConfig()->getSetting('LIBRARY_LOCATION') . 'temp/' . $fileName);
// Set the name for the return
$file->name = $layout->layout;
$file->id = $layout->layoutId;
} catch (Exception $e) {
$controller->getLog()->error(sprintf('Error importing Layout: %s', $e->getMessage()));
$controller->getLog()->debug($e->getTraceAsString());
$file->error = $e->getMessage();
// Don't commit
$controller->getState()->setCommitState(false);
}
}
}

167
lib/Helper/LinkSigner.php Normal file
View File

@@ -0,0 +1,167 @@
<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use Xibo\Entity\Display;
/**
* S3 style links
* inspired by https://gist.github.com/kelvinmo/d78be66c4f36415a6b80
*/
class LinkSigner
{
/**
* @param \Xibo\Entity\Display $display
* @param string $encryptionKey
* @param string|null $cdnUrl
* @param $type
* @param $itemId
* @param string $storedAs
* @param string|null $fileType
* @return string
* @throws \Xibo\Support\Exception\NotFoundException
*/
public static function generateSignedLink(
Display $display,
string $encryptionKey,
?string $cdnUrl,
$type,
$itemId,
string $storedAs,
string $fileType = null,
bool $isRequestFromPwa = false,
): string {
// Start with the base url, which should correctly account for running with a CMS_ALIAS
$xmdsRoot = (new HttpsDetect())->getBaseUrl();
// PWA requests resources via `/pwa/getResource`, but the link should be served from `/xmds.php`
if ($isRequestFromPwa) {
$xmdsRoot = str_replace('/pwa/getResource', '/xmds.php', $xmdsRoot);
}
// Build the rest of the URL
$saveAsPath = $xmdsRoot
. '?file=' . $storedAs
. '&displayId=' . $display->displayId
. '&type=' . $type
. '&itemId=' . $itemId;
if ($fileType !== null) {
$saveAsPath .= '&fileType=' . $fileType;
}
$saveAsPath .= '&' . LinkSigner::getSignature(
parse_url($xmdsRoot, PHP_URL_HOST),
$storedAs,
time() + ($display->getSetting('collectionInterval', 300) * 2),
$encryptionKey,
);
// CDN?
if (!empty($cdnUrl)) {
// Serve a link to the CDN
// CDN_URL has a `?dl=` parameter on the end already, so we just encode our string and concatenate it
return 'http' . (
(
(isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on') ||
(isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
&& strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https')
) ? 's' : '')
. '://' . $cdnUrl . urlencode($saveAsPath);
} else {
// Serve a HTTP link to XMDS
return $saveAsPath;
}
}
/**
* Get a S3 compatible signature
*/
public static function getSignature(
string $host,
string $uri,
int $expires,
string $secretKey,
?string $timeText = null,
?bool $isReturnSignature = false
): string {
$encodedUri = str_replace('%2F', '/', rawurlencode($uri));
$headerString = 'host:' . $host . "\n";
$signedHeadersString = 'host';
if ($timeText === null) {
$timestamp = time();
$dateText = gmdate('Ymd', $timestamp);
$timeText = $dateText . 'T000000Z';
} else {
$dateText = explode('T', $timeText)[0];
}
$algorithm = 'AWS4-HMAC-SHA256';
$scope = $dateText . '/all/s3/aws4_request';
$amzParams = [
'X-Amz-Algorithm' => $algorithm,
'X-Amz-Date' => $timeText,
'X-Amz-SignedHeaders' => $signedHeadersString
];
if ($expires > 0) {
$amzParams['X-Amz-Expires'] = $expires;
}
ksort($amzParams);
$queryStringItems = [];
foreach ($amzParams as $key => $value) {
$queryStringItems[] = rawurlencode($key) . '=' . rawurlencode($value);
}
$queryString = implode('&', $queryStringItems);
$request = 'GET' . "\n" . $encodedUri . "\n" . $queryString . "\n" . $headerString . "\n"
. $signedHeadersString . "\nUNSIGNED-PAYLOAD";
$stringToSign = $algorithm . "\n" . $timeText . "\n" . $scope . "\n" . hash('sha256', $request);
$signingKey = hash_hmac(
'sha256',
'aws4_request',
hash_hmac(
'sha256',
's3',
hash_hmac(
'sha256',
'all',
hash_hmac(
'sha256',
$dateText,
'AWS4' . $secretKey,
true
),
true
),
true
),
true
);
$signature = hash_hmac('sha256', $stringToSign, $signingKey);
return ($isReturnSignature) ? $signature : $queryString . '&X-Amz-Signature=' . $signature;
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Copyright (C) 2021 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use Slim\Http\ServerRequest as Request;
use Xibo\Entity\User;
use Xibo\Service\LogServiceInterface;
trait LogoutTrait
{
public function completeLogoutFlow(User $user, Session $session, LogServiceInterface $log, Request $request)
{
$user->touch();
unset($_SESSION['userid']);
unset($_SESSION['username']);
unset($_SESSION['password']);
$session->setIsExpired(1);
$log->audit('User', $user->userId, 'User logout', [
'UserAgent' => $request->getHeader('User-Agent')
]);
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* Copyright (C) 2021 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
class NatoAlphabet
{
public static function convertToNato($word) {
$replacement = [
"a"=>"Alpha", "b"=>"Bravo", "c"=>"Charlie",
"d"=>"Delta", "e"=>"Echo", "f"=>"Foxtrot",
"g"=>"Golf", "h"=>"Hotel", "i"=>"India",
"j"=>"Juliet", "k"=>"Kilo", "l"=>"Lima",
"m"=>"Mike", "n"=>"November", "o"=>"Oscar",
"p"=>"Papa", "q"=>"Quebec", "r"=>"Romeo",
"s"=>"Sierra", "t"=>"Tango", "u"=>"Uniform",
"v"=>"Victor", "w"=>"Whiskey", "x"=>"X-Ray",
"y"=>"Yankee", "z"=>"Zulu", "0"=>"Zero",
"1"=>"One", "2"=>"Two", "3"=>"Three",
"4"=>"Four", "5"=>"Five", "6"=>"Six",
"7"=>"Seven", "8"=>"Eight", "9"=>"Nine",
"-"=>"Dash", " "=>"(Space)"
];
$converted = [];
for ($i=0; $i < strlen($word); $i++) {
$currentLetter = substr($word, $i, 1);
if (!empty($replacement[$currentLetter])) {
$convertedWord = strtolower($replacement[$currentLetter]);
} elseif (!empty($replacement[strtolower($currentLetter)])) {
$convertedWord = $replacement[strtolower($currentLetter)];
} else {
$convertedWord = $currentLetter;
}
$converted[] = $convertedWord;
}
return implode(' ', $converted);
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* Copyright (C) 2021 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
class NullHelpService
{
/**
* @inheritdoc
*/
public function link($topic = null, $category = 'General')
{
//
}
/**
* @inheritdoc
*/
public function address($suffix = '')
{
//
}
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
class NullSanitizer extends SanitizerService
{
/**
* @param $array
*/
public function getSanitizer($array)
{
//
}
/**
*/
public function getValidator()
{
//
}
}

View File

@@ -0,0 +1,66 @@
<?php
/*
* Spring Signage Ltd - http://www.springsignage.com
* Copyright (C) 2015 Spring Signage Ltd
* (NullSession.php)
*/
namespace Xibo\Helper;
class NullSession
{
/**
* Set UserId
* @param $userId
*/
function setUser($userId)
{
$_SESSION['userid'] = $userId;
}
/**
* Updates the session ID with a new one
*/
public function regenerateSessionId()
{
}
/**
* Set Expired
* @param $isExpired
*/
function setIsExpired($isExpired)
{
}
/**
* Store a variable in the session
* @param string $key
* @param mixed $secondKey
* @param mixed|null $value
* @return mixed
*/
public static function set($key, $secondKey, $value = null)
{
if (func_num_args() == 2) {
return $secondKey;
} else {
return $value;
}
}
/**
* Get the Value from the position denoted by the 2 keys provided
* @param string $key
* @param string [Optional] $secondKey
* @return bool
*/
public static function get($key, $secondKey = NULL)
{
return false;
}
}

36
lib/Helper/NullView.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
/**
* Copyright (C) 2021 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
class NullView
{
public function fetch(string $template, array $data = [])
{
//
}
public function render($response, string $template, array $data = [])
{
//
}
}

37
lib/Helper/ObjectVars.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2015 Spring Signage Ltd
*
* This file (ObjectVars.php) is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
class ObjectVars
{
/**
* Get Object Properties
* @param $object
* @return array
*/
public static function getObjectVars($object)
{
return get_object_vars($object);
}
}

123
lib/Helper/Pbkdf2Hash.php Normal file
View File

@@ -0,0 +1,123 @@
<?php
/*
* Spring Signage Ltd - http://www.springsignage.com
* Copyright (C) 2016 Spring Signage Ltd
* (PasswordStorage.php)
*/
namespace Xibo\Helper;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class Pbkdf2Hash
* @package Xibo\Helper
*/
class Pbkdf2Hash
{
// These constants may be changed without breaking existing hashes.
const PBKDF2_HASH_ALGORITHM = 'sha256';
const PBKDF2_ITERATIONS = 1000;
const PBKDF2_SALT_BYTES = 24;
const PBKDF2_HASH_BYTES = 24;
// These constants define the encoding and may not be changed.
const HASH_SECTIONS = 4;
const HASH_ALGORITHM_INDEX = 0;
const HASH_ITERATION_INDEX = 1;
const HASH_SALT_INDEX = 2;
const HASH_PBKDF2_INDEX = 3;
/**
* @param string $password
* @param string $hash
* @return bool
* @throws InvalidArgumentException
*/
public static function verifyPassword($password, $hash)
{
$params = explode(':', $hash);
if (count($params) < self::HASH_SECTIONS) {
throw new InvalidArgumentException(__('Invalid password hash - not enough hash sections'));
}
$pbkdf2 = base64_decode($params[self::HASH_PBKDF2_INDEX]);
// Check to see if the hash created from the provided password is the same as the hash we have stored already
return (self::slowEquals(
$pbkdf2,
self::pbkdf2(
$params[self::HASH_ALGORITHM_INDEX],
$password,
$params[self::HASH_SALT_INDEX],
(int)$params[self::HASH_ITERATION_INDEX],
strlen($pbkdf2),
true
)
));
}
/**
* Compares two strings $a and $b in length-constant time.
* @param string $a
* @param string $b
* @return bool
*/
private static function slowEquals($a, $b)
{
$diff = strlen($a) ^ strlen($b);
for($i = 0; $i < strlen($a) && $i < strlen($b); $i++)
{
$diff |= ord($a[$i]) ^ ord($b[$i]);
}
return $diff === 0;
}
/**
* PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt
*
* Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt
*
* This implementation of PBKDF2 was originally created by https://defuse.ca
* With improvements by http://www.variations-of-shadow.com
*
* @param string $algorithm The hash algorithm to use. Recommended: SHA256
* @param string $password The password.
* @param string $salt A salt that is unique to the password.
* @param int $count Iteration count. Higher is better, but slower. Recommended: At least 1000.
* @param int $key_length The length of the derived key in bytes.
* @param bool $raw_output If true, the key is returned in raw binary format. Hex encoded otherwise.
* @return string A $key_length-byte key derived from the password and salt.
* @throws InvalidArgumentException
*/
public static function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)
{
$algorithm = strtolower($algorithm);
if (!in_array($algorithm, hash_algos(), true))
throw new InvalidArgumentException('PBKDF2 ERROR: Invalid hash algorithm.');
if ($count <= 0 || $key_length <= 0)
throw new InvalidArgumentException('PBKDF2 ERROR: Invalid parameters.');
$hash_length = strlen(hash($algorithm, "", true));
$block_count = ceil($key_length / $hash_length);
$output = "";
for ($i = 1; $i <= $block_count; $i++) {
// $i encoded as 4 bytes, big endian.
$last = $salt . pack("N", $i);
// first iteration
$last = $xorsum = hash_hmac($algorithm, $last, $password, true);
// perform the other $count - 1 iterations
for ($j = 1; $j < $count; $j++) {
$xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
}
$output .= $xorsum;
}
if ($raw_output)
return substr($output, 0, $key_length);
else
return bin2hex(substr($output, 0, $key_length));
}
}

62
lib/Helper/Profiler.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
/*
* Copyright (C) 2021 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
/**
* Class Profiler
* @package Xibo\Helper
*/
class Profiler
{
private static $profiles = [];
/**
* @param $key
* @param null $logger
*/
public static function start($key, $logger = null)
{
$start = microtime(true);
self::$profiles[$key] = $start;
if ($logger !== null) {
$logger->debug('PROFILE: ' . $key . ' - start: ' . $start);
}
}
/**
* @param $key
* @param null $logger
*/
public static function end($key, $logger = null)
{
$start = self::$profiles[$key] ?? 0;
$end = microtime(true);
unset(self::$profiles[$key]);
if ($logger !== null) {
$logger->debug('PROFILE: ' . $key . ' - end: ' . $end
. ', duration: ' . ($end - $start));
}
}
}

View File

@@ -0,0 +1,104 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use RobThree\Auth\Providers\Qr\BaseHTTPQRCodeProvider;
use RobThree\Auth\Providers\Qr\QRException;
class QuickChartQRProvider extends BaseHTTPQRCodeProvider
{
public $url;
public $errorCorrectionLevel;
public $margin;
public $backgroundColor;
public $color;
public $format;
/**
* QuickChartQRProvider constructor.
* @param string $url URL to a Quick Chart service
* @param bool $verifyssl
* @param string $errorCorrectionLevel valid values L, M, Q, H
* @param int $margin
* @param string $backgroundColor Hex color code - background colour
* @param string $color Hex color code - QR colour
* @param string $format Valid values: png, svg
* @throws QRException
*/
public function __construct(
$url,
$verifyssl = false,
$errorCorrectionLevel = 'L',
$margin = 4,
$backgroundColor = 'ffffff',
$color = '000000',
$format = 'png'
) {
if (!is_bool($verifyssl)) {
throw new QRException('VerifySSL must be bool');
}
$this->verifyssl = $verifyssl;
$this->url = $url;
$this->errorCorrectionLevel = $errorCorrectionLevel;
$this->margin = $margin;
$this->backgroundColor = $backgroundColor;
$this->color = $color;
$this->format = $format;
}
/**
* @return string
* @throws QRException
*/
public function getMimeType()
{
switch (strtolower($this->format)) {
case 'png':
return 'image/png';
case 'svg':
return 'image/svg+xml';
case 'webp':
return 'image/webp';
}
throw new QRException(sprintf('Unknown MIME-type: %s', $this->format));
}
public function getQRCodeImage($qrText, $size)
{
return $this->getContent($this->getUrl($qrText, $size));
}
public function getUrl($qrText, $size)
{
return $this->url . '/qr'
. '?size=' . $size
. '&ecLevel=' . strtoupper($this->errorCorrectionLevel)
. '&margin=' . $this->margin
. '&light=' . $this->backgroundColor
. '&dark=' . $this->color
. '&format=' . strtolower($this->format)
. '&text=' . rawurlencode($qrText);
}
}

51
lib/Helper/Random.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
/**
* Class Random
* @package Xibo\Helper
*/
class Random
{
/**
* @param int $length
* @param string $prefix
* @return string
* @throws \Exception
*/
public static function generateString($length = 10, $prefix = '')
{
if (function_exists('random_bytes')) {
return substr($prefix . bin2hex(random_bytes($length)), 0, $length + strlen($prefix));
} else {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $prefix . $randomString;
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
/**
* Class RouteLogProcessor
* a process to add route/method information to the log record
* @package Xibo\Helper
*/
class RouteLogProcessor
{
/**
* Log Processor
* @param string $route
* @param string $method
*/
public function __construct(
private readonly string $route,
private readonly string $method
) {
}
/**
* @param array $record
* @return array
*/
public function __invoke(array $record): array
{
$record['extra']['method'] = $this->method;
$record['extra']['route'] = $this->route;
return $record;
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use Xibo\Support\Sanitizer\RespectSanitizer;
use Xibo\Support\Sanitizer\SanitizerInterface;
use Xibo\Support\Validator\RespectValidator;
use Xibo\Support\Validator\ValidatorInterface;
class SanitizerService
{
/**
* @param $array
* @return SanitizerInterface
*/
public function getSanitizer($array)
{
return (new RespectSanitizer())
->setCollection($array)
->setDefaultOptions([
'checkboxReturnInteger' => true
]);
}
/**
* @return ValidatorInterface
*/
public function getValidator()
{
return new RespectValidator();
}
}

67
lib/Helper/SendFile.php Normal file
View File

@@ -0,0 +1,67 @@
<?php
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use GuzzleHttp\Psr7\Stream;
use Slim\Http\Response;
/**
* Class SendFile
* @package Xibo\Helper
*/
class SendFile
{
/**
* @param \Slim\Http\Response $response
* @param string $sendFile
* @param string $filePath
* @param string|null $name
* @param bool $zlibOff
* @return \Slim\Http\Response
*/
public static function decorateResponse($response, $sendFile, $filePath, $name = null, $zlibOff = true):? Response
{
if ($zlibOff && ini_get('zlib.output_compression')) {
ini_set('zlib.output_compression', 'Off');
}
$baseName = basename($filePath);
$response = $response
->withHeader('Content-Type', 'application/octet-stream')
->withHeader('Content-Disposition', 'attachment; filename=' . ($name === null ? $baseName : $name))
->withHeader('Content-Transfer-Encoding', 'Binary')
->withHeader('Content-Length', filesize($filePath));
// Send via Apache X-Sendfile header?
if ($sendFile == 'Apache') {
$response = $response->withHeader('X-Sendfile', $filePath);
} else if ($sendFile == 'Nginx') {
// Send via Nginx X-Accel-Redirect?
$response = $response->withHeader('X-Accel-Redirect', '/download/temp/' . $baseName);
} else {
$response = $response->withBody(new Stream(fopen($filePath, 'r')));
}
return $response;
}
}

547
lib/Helper/Session.php Normal file
View File

@@ -0,0 +1,547 @@
<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use Carbon\Carbon;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\PdoStorageService;
/**
* Class Session
* @package Xibo\Helper
*/
class Session implements \SessionHandlerInterface
{
private $maxLifetime;
private $key;
/**
* Refresh expiry
* @var bool
*/
public $refreshExpiry = true;
/**
* Expiry time
* @var int
*/
private $sessionExpiry = 0;
/**
* Is the session expired?
* @var bool
*/
private $expired = true;
/**
* The UserId whom owns this session
* @var int
*/
private $userId = 0;
/**
* @var bool Whether gc() has been called
*/
private $gcCalled = false;
/**
* Prune this key?
* @var bool
*/
private $pruneKey = false;
/**
* The database connection
* @var PdoStorageService
*/
private $pdo = null;
/**
* Log
* @var LogServiceInterface
*/
private LogServiceInterface $log;
/**
* Session constructor.
* @param LogServiceInterface $log
*/
public function __construct(LogServiceInterface $log)
{
$this->log = $log;
}
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName): bool
{
//$this->log->debug('Session open');
$this->maxLifetime = ini_get('session.gc_maxlifetime');
return true;
}
/**
* {@inheritdoc}
*/
public function close(): bool
{
//$this->log->debug('Session close');
try {
// Commit
$this->commit();
} catch (\PDOException $e) {
$this->log->error('Error closing session: %s', $e->getMessage());
}
try {
// Prune this session if necessary
if ($this->pruneKey || $this->gcCalled) {
$db = new PdoStorageService($this->log);
$db->setConnection();
if ($this->pruneKey) {
$db->update('DELETE FROM `session` WHERE session_id = :session_id', [
'session_id' => $this->key,
]);
}
if ($this->gcCalled) {
// Delete sessions older than 10 times the max lifetime
$db->update('DELETE FROM `session` WHERE IsExpired = 1 AND session_expiration < :expiration', [
'expiration' => Carbon::now()->subSeconds($this->maxLifetime * 10)->format('U'),
]);
// Update expired sessions as expired
$db->update('UPDATE `session` SET IsExpired = 1 WHERE session_expiration < :expiration', [
'expiration' => Carbon::now()->format('U'),
]);
}
$db->commitIfNecessary();
$db->close();
}
} catch (\PDOException $e) {
$this->log->error('Error closing session: %s', $e->getMessage());
}
// Close
$this->getDb()->close();
return true;
}
/**
* {@inheritdoc}
*/
public function read($key): false|string
{
//$this->log->debug('Session read');
$data = '';
$this->key = $key;
$userAgent = substr(htmlspecialchars($_SERVER['HTTP_USER_AGENT']), 0, 253);
try {
$dbh = $this->getDb();
// Start a transaction
$this->beginTransaction();
// Get this session
$sth = $dbh->getConnection()->prepare('
SELECT `session_data`, `isexpired`, `useragent`, `session_expiration`, `userId`
FROM `session`
WHERE `session_id` = :session_id
');
$sth->execute(['session_id' => $key]);
$row = $sth->fetch();
if (!$row) {
// New session.
$this->insertSession(
$key,
'',
Carbon::now()->format('U'),
Carbon::now()->addSeconds($this->maxLifetime)->format('U'),
);
$this->expired = false;
} else {
// Existing session
// Check the session hasn't expired
if ($row['session_expiration'] < Carbon::now()->format('U')) {
$this->expired = true;
} else {
$this->expired = $row['isexpired'];
}
// What happens if the UserAgent has changed?
if ($row['useragent'] != $userAgent) {
// Force delete this session
$this->expired = 1;
$this->pruneKey = true;
}
$this->userId = $row['userId'];
$this->sessionExpiry = $row['session_expiration'];
// Set the session data (expired or not)
$data = $row['session_data'];
}
return (string)$data;
} catch (\Exception $e) {
$this->log->error('Error reading session: %s', $e->getMessage());
return $data;
}
}
/**
* {@inheritdoc}
*/
public function write($id, $data): bool
{
//$this->log->debug('Session write');
// What should we do with expiry?
$expiry = ($this->refreshExpiry)
? Carbon::now()->addSeconds($this->maxLifetime)->format('U')
: $this->sessionExpiry;
try {
$this->updateSession($id, $data, Carbon::now()->format('U'), $expiry);
} catch (\PDOException $e) {
$this->log->error('Error writing session data: %s', $e->getMessage());
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function destroy($id): bool
{
//$this->log->debug('Session destroy');
try {
$this->getDb()->update('DELETE FROM `session` WHERE session_id = :session_id', ['session_id' => $id]);
} catch (\PDOException $e) {
$this->log->error('Error destroying session: %s', $e->getMessage());
}
return true;
}
/**
* {@inheritdoc}
*/
public function gc($max_lifetime): false|int
{
//$this->log->debug('Session gc');
$this->gcCalled = true;
return true;
}
/**
* Sets the User Id
* @param $userId
*/
public function setUser($userId): void
{
//$this->log->debug('Setting user Id to %d', $userId);
$_SESSION['userid'] = $userId;
$this->userId = $userId;
}
/**
* Updates the session ID with a new one
*/
public function regenerateSessionId(): void
{
//$this->log->debug('Session regenerate');
session_regenerate_id(true);
$this->key = session_id();
}
/**
* Set this session to expired
* @param $isExpired
*/
public function setIsExpired($isExpired): void
{
$this->expired = $isExpired;
}
/**
* Store a variable in the session
* @param string $key
* @param mixed $secondKey
* @param mixed|null $value
* @return mixed
*/
public static function set(string $key, mixed $secondKey, mixed $value = null): mixed
{
if (func_num_args() == 2) {
$_SESSION[$key] = $secondKey;
return $secondKey;
} else {
if (!isset($_SESSION[$key]) || !is_array($_SESSION[$key])) {
$_SESSION[$key] = [];
}
$_SESSION[$key][(string) $secondKey] = $value;
return $value;
}
}
/**
* Get the Value from the position denoted by the 2 keys provided
* @param string $key
* @param string $secondKey
* @return bool
*/
public static function get(string $key, ?string $secondKey = null): mixed
{
if ($secondKey != null) {
if (isset($_SESSION[$key][$secondKey])) {
return $_SESSION[$key][$secondKey];
}
} else {
if (isset($_SESSION[$key])) {
return $_SESSION[$key];
}
}
return false;
}
/**
* Is the session expired?
* @return bool
*/
public function isExpired(): bool
{
return $this->expired;
}
/**
* Get a Database
* @return PdoStorageService
*/
private function getDb(): PdoStorageService
{
if ($this->pdo == null) {
$this->pdo = (new PdoStorageService($this->log))->setConnection();
}
return $this->pdo;
}
/**
* Helper method to begin a transaction.
*
* MySQLs default isolation, REPEATABLE READ, causes deadlock for different sessions
* due to http://www.mysqlperformanceblog.com/2013/12/12/one-more-innodb-gap-lock-to-avoid/ .
* So we change it to READ COMMITTED.
*/
private function beginTransaction(): void
{
if (!$this->getDb()->getConnection()->inTransaction()) {
try {
$this->getDb()->getConnection()->exec('SET TRANSACTION ISOLATION LEVEL READ COMMITTED');
} catch (\PDOException $e) {
// https://github.com/xibosignage/xibo/issues/787
// this only works if BINLOG format is set to MIXED or ROW
$this->log->error('Unable to set session transaction isolation level, message = ' . $e->getMessage());
}
$this->getDb()->getConnection()->beginTransaction();
}
}
/**
* Commit
*/
private function commit(): void
{
if ($this->getDb()->getConnection()->inTransaction()) {
$this->getDb()->getConnection()->commit();
}
}
/**
* Insert session
* @param $key
* @param $data
* @param $lastAccessed
* @param $expiry
*/
private function insertSession($key, $data, $lastAccessed, $expiry): void
{
//$this->log->debug('Session insert');
$this->insertSessionHistory();
$sql = '
INSERT INTO `session` (
`session_id`,
`session_data`,
`session_expiration`,
`lastaccessed`,
`userid`,
`isexpired`,
`useragent`,
`remoteaddr`
)
VALUES (
:session_id,
:session_data,
:session_expiration,
:lastAccessed,
:userId,
:expired,
:useragent,
:remoteaddr
)
';
$params = [
'session_id' => $key,
'session_data' => $data,
'session_expiration' => $expiry,
'lastAccessed' => Carbon::createFromTimestamp($lastAccessed)->format(DateFormatHelper::getSystemFormat()),
'userId' => $this->userId,
'expired' => ($this->expired) ? 1 : 0,
'useragent' => substr(htmlspecialchars($_SERVER['HTTP_USER_AGENT']), 0, 253),
'remoteaddr' => $this->getIp()
];
$this->getDb()->update($sql, $params);
}
private function insertSessionHistory(): void
{
$sql = '
INSERT INTO `session_history` (`ipAddress`, `userAgent`, `startTime`, `userId`, `lastUsedTime`)
VALUES (:ipAddress, :userAgent, :startTime, :userId, :lastUsedTime)
';
$params = [
'ipAddress' => $this->getIp(),
'userAgent' => substr(htmlspecialchars($_SERVER['HTTP_USER_AGENT']), 0, 253),
'startTime' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
'userId' => $this->userId,
'lastUsedTime' => Carbon::now()->format(DateFormatHelper::getSystemFormat())
];
$id = $this->getDb()->insert($sql, $params);
$this->set('sessionHistoryId', $id);
}
/**
* Update Session
* @param $key
* @param $data
* @param $lastAccessed
* @param $expiry
*/
private function updateSession($key, $data, $lastAccessed, $expiry): void
{
//$this->log->debug('Session update');
$this->updateSessionHistory();
$sql = '
UPDATE `session` SET
session_data = :session_data,
session_expiration = :session_expiration,
LastAccessed = :lastAccessed,
userID = :userId,
IsExpired = :expired
WHERE session_id = :session_id
';
$params = [
'session_data' => $data,
'session_expiration' => $expiry,
'lastAccessed' => Carbon::createFromTimestamp($lastAccessed)->format(DateFormatHelper::getSystemFormat()),
'userId' => $this->userId,
'expired' => ($this->expired) ? 1 : 0,
'session_id' => $key
];
$this->getDb()->update($sql, $params);
}
/**
* Updates the session history
*/
private function updateSessionHistory(): void
{
$sql = '
UPDATE `session_history` SET
lastUsedTime = :lastUsedTime, userID = :userId
WHERE sessionId = :sessionId
';
$params = [
'lastUsedTime' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
'userId' => $this->userId,
'sessionId' => $_SESSION['sessionHistoryId'],
];
$this->getDb()->update($sql, $params);
}
/**
* Get the Client IP Address
* @return string
*/
private function getIp(): string
{
$clientIp = '';
$keys = array('X_FORWARDED_FOR', 'HTTP_X_FORWARDED_FOR', 'CLIENT_IP', 'REMOTE_ADDR');
foreach ($keys as $key) {
if (isset($_SERVER[$key]) && filter_var($_SERVER[$key], FILTER_VALIDATE_IP) !== false) {
$clientIp = $_SERVER[$key];
break;
}
}
return $clientIp;
}
/**
* @param $userId
*/
public function expireAllSessionsForUser($userId): void
{
$this->getDb()->update('UPDATE `session` SET IsExpired = 1 WHERE userID = :userId', [
'userId' => $userId
]);
}
}

35
lib/Helper/Status.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
/**
* Static class to reference statuses.
*/
class Status
{
// Widget statuses.
public static $STATUS_VALID = 1;
public static $STATUS_PLAYER = 2;
public static $STATUS_NOT_BUILT = 3;
public static $STATUS_INVALID = 4;
}

238
lib/Helper/Translate.php Normal file
View File

@@ -0,0 +1,238 @@
<?php
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2015 Spring Signage Ltd
*
* This file (TranslationEngine.php) is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use CachedFileReader;
use Gettext\Translations;
use Gettext\Translator;
use gettext_reader;
use Illuminate\Support\Str;
use Xibo\Service\ConfigServiceInterface;
/**
* Class Translate
* @package Xibo\Helper
*/
class Translate
{
private static $requestedLanguage;
private static $locale;
private static $jsLocale;
private static $jsLocaleRequested;
/**
* Gets and Sets the Locale
* @param ConfigServiceInterface $config
* @param $language string[optional] The Language to Load
*/
public static function InitLocale($config, $language = NULL)
{
// The default language
$default = ($language === null) ? $config->getSetting('DEFAULT_LANGUAGE') : $language;
// Build an array of supported languages
$localeDir = PROJECT_ROOT . '/locale';
$supportedLanguages = array_map('basename', glob($localeDir . '/*.mo'));
// Record any matching languages we find.
$foundLanguage = null;
// Try to get the local firstly from _REQUEST (post then get)
if ($language != null) {
// Serve only the requested language
// Firstly, Sanitize it
self::$requestedLanguage = str_replace('-', '_', $language);
// Check its valid
if (in_array(self::$requestedLanguage . '.mo', $supportedLanguages)) {
$foundLanguage = self::$requestedLanguage;
}
}
else if ($config->getSetting('DETECT_LANGUAGE') == 1) {
// Detect the language, try from HTTP accept
// Parse the language header and build a preference array
$languagePreferenceArray = Translate::parseHttpAcceptLanguageHeader();
if (count($languagePreferenceArray) > 0) {
// Go through the list until we have a match
foreach ($languagePreferenceArray as $languagePreference => $preferenceRating) {
// We don't ship an en.mo, so fudge in a case where we automatically convert that to en_GB
if ($languagePreference == 'en')
$languagePreference = 'en_GB';
// Sanitize
$languagePreference = str_replace('-', '_', $languagePreference);
// Set as requested
self::$requestedLanguage = $languagePreference;
// Check it is valid
if (in_array($languagePreference . '.mo', $supportedLanguages)) {
$foundLanguage = $languagePreference;
break;
}
}
}
}
// Requested language
if (self::$requestedLanguage == null)
self::$requestedLanguage = $default;
// Are we still empty, then default language from settings
if ($foundLanguage == '') {
// Check the default
if (!in_array($default . '.mo', $supportedLanguages)) {
$default = 'en_GB';
}
// The default is valid
$foundLanguage = $default;
}
// Load translations
$translator = new Translator();
$translator->loadTranslations(Translations::fromMoFile($localeDir . '/' . $foundLanguage . '.mo'));
$translator->register();
// Store our resolved language locales
self::$locale = $foundLanguage;
self::$jsLocale = str_replace('_', '-', $foundLanguage);
self::$jsLocaleRequested = str_replace('_', '-', self::$requestedLanguage);
}
/**
* Get translations for user selected language
* @param $language
* @return Translator|null
*/
public static function getTranslationsFromLocale($language): ?Translator
{
// Build an array of supported languages
$localeDir = PROJECT_ROOT . '/locale';
$supportedLanguages = array_map('basename', glob($localeDir . '/*.mo'));
// Record any matching languages we find.
$foundLanguage = null;
// Try to get the local firstly from _REQUEST (post then get)
if ($language != null) {
$parsedLanguage = str_replace('-', '_', $language);
// Check its valid
if (in_array($parsedLanguage . '.mo', $supportedLanguages)) {
$foundLanguage = $parsedLanguage;
} else {
return null;
}
}
// Are we still empty, then return null
if ($foundLanguage == '') {
return null;
}
// Load translations
$translator = new Translator();
$translator->loadTranslations(Translations::fromMoFile($localeDir . '/' . $foundLanguage . '.mo'));
$translator->register();
return $translator;
}
/**
* Get the Locale
* @param null $characters The number of characters to take from the beginning of the local string
* @return mixed
*/
public static function GetLocale($characters = null)
{
return ($characters == null) ? self::$locale : substr(self::$locale, 0, $characters);
}
public static function GetJsLocale()
{
return self::$jsLocale;
}
/**
* @param array $options
* @return string
*/
public static function getRequestedJsLocale($options = [])
{
$options = array_merge([
'short' => false
], $options);
if ($options['short'] && (strlen(self::$jsLocaleRequested) > 2) && Str::contains(self::$jsLocaleRequested, '-')) {
// Short js-locale requested, and our string is longer than 2 characters and has a splitter (language variant)
$variant = explode('-', self::$jsLocaleRequested);
// The logic here is that if they are the same, i.e. de-DE, then we should only output de, but if they are
// different, i.e. de-AT then we should output the whole thing
return (strtolower($variant[0]) === strtolower($variant[1])) ? $variant[0] : self::$jsLocaleRequested;
} else {
return self::$jsLocaleRequested;
}
}
public static function getRequestedLanguage()
{
return self::$requestedLanguage;
}
/**
* Parse the HttpAcceptLanguage Header
* Inspired by: http://www.thefutureoftheweb.com/blog/use-accept-language-header
* @param null $header
* @return array Language array where the key is the language identifier and the value is the preference double.
*/
public static function parseHttpAcceptLanguageHeader($header = null)
{
if ($header == null)
$header = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '';
$languages = array();
if ($header != '') {
// break up string into pieces (languages and q factors)
preg_match_all('/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i', $header, $langParse);
if (count($langParse[1])) {
// create a list like "en" => 0.8
$languages = array_combine($langParse[1], $langParse[4]);
// set default to 1 for any without q factor
foreach ($languages as $lang => $val) {
if ($val === '')
$languages[$lang] = 1;
}
// sort list based on value
arsort($languages, SORT_NUMERIC);
}
}
return $languages;
}
}

View File

@@ -0,0 +1,124 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use Xibo\Support\Exception\LibraryFullException;
/**
* @phpcs:disable PSR1.Methods.CamelCapsMethodName
*/
class UploadHandler extends BlueImpUploadHandler
{
/**
* @var callable
*/
private $postProcess;
/** @var ApplicationState */
private $state;
/**
* Set post processor
* @param callable $function
*/
public function setPostProcessor(callable $function)
{
$this->postProcess = $function;
}
/**
* @param ApplicationState $state
* @return $this
*/
public function setState(ApplicationState $state)
{
$this->state = $state;
return $this;
}
/**
* Handle form data from BlueImp
* @param $file
* @param $index
*/
protected function handleFormData($file, $index)
{
try {
$filePath = $this->getUploadDir() . $file->name;
$file->fileName = $file->name;
$name = htmlspecialchars($this->getParam($index, 'name', $file->name));
$file->name = $name;
// Check Library
if ($this->options['libraryQuotaFull']) {
throw new LibraryFullException(
sprintf(
__('Your library is full. Library Limit: %s K'),
$this->options['libraryLimit']
)
);
}
$this->getLogger()->debug('Upload complete for name: ' . $name . '. Index is ' . $index);
if ($this->postProcess !== null) {
$file = call_user_func($this->postProcess, $file, $this);
}
} catch (\Exception $exception) {
$this->getLogger()->error('Error uploading file : ' . $exception->getMessage());
$this->getLogger()->debug($exception->getTraceAsString());
// Unlink the temporary file
@unlink($filePath);
$this->state->setCommitState(false);
$file->error = $exception->getMessage();
}
return $file;
}
/**
* Get Param from File Input, taking into account multi-upload index if applicable
* @param int $index
* @param string $param
* @param mixed $default
* @return mixed
*/
private function getParam($index, $param, $default)
{
if ($index === null) {
if (isset($_REQUEST[$param])) {
return $_REQUEST[$param];
} else {
return $default;
}
} else {
if (isset($_REQUEST[$param][$index])) {
return $_REQUEST[$param][$index];
} else {
return $default;
}
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
/**
* Class UserLogProcessor
* @package Xibo\Helper
*/
class UserLogProcessor
{
/**
* UserLogProcessor
* @param int $userId
* @param int|null $sessionHistoryId
* @param int|null $requestId
*/
public function __construct(
private readonly int $userId,
private readonly ?int $sessionHistoryId,
private readonly ?int $requestId
) {
}
/**
* @param array $record
* @return array
*/
public function __invoke(array $record): array
{
$record['extra']['userId'] = $this->userId;
if ($this->sessionHistoryId != null) {
$record['extra']['sessionHistoryId'] = $this->sessionHistoryId;
}
if ($this->requestId != null) {
$record['extra']['requestId'] = $this->requestId;
}
return $record;
}
}

271
lib/Helper/WakeOnLan.php Normal file
View File

@@ -0,0 +1,271 @@
<?php
/*
* Spring Signage Ltd - http://www.springsignage.com
* Copyright (C) 2015 Spring Signage Ltd
* (WakeOnLan.php)
*/
namespace Xibo\Helper;
use Xibo\Service\LogServiceInterface;
class WakeOnLan
{
/**
* Wake On Lan Script
* @param string $macAddress
* @param string $secureOn
* @param string $address
* @param int $cidr
* @param int $port
* @param LogServiceInterface $logger
* @version 2
* @author DS508_customer (http://www.synology.com/enu/forum/memberlist.php?mode=viewprofile&u=12636)
* Please inform the author of any suggestions on (the functionality, graphical design, ... of) this application.
* More info: http://wolviaphp.sourceforge.net
* @licence GPLv2.0
* @throws \Exception
*
* Modified for use with the Xibo project by Dan Garner.
*/
public static function TransmitWakeOnLan($macAddress, $secureOn, $address, $cidr, $port, $logger) {
// Prepare magic packet: part 1/3 (defined constant)
$buf = "";
// the defined constant as represented in hexadecimal: FF FF FF FF FF FF (i.e., 6 bytes of hexadecimal FF)
for ($a=0; $a<6; $a++) $buf .= chr(255);
// Check whether $mac_address is valid
$macAddress = strtoupper($macAddress);
$macAddress = str_replace(":", "-", $macAddress);
if ((!preg_match("/([A-F0-9]{2}[-]){5}([0-9A-F]){2}/",$macAddress)) || (strlen($macAddress) != 17))
{
throw new \Exception(__('Pattern of MAC-address is not "xx-xx-xx-xx-xx-xx" (x = digit or letter)'));
}
else
{
// Prepare magic packet: part 2/3 (16 times MAC-address)
// Split MAC-address into an array of (six) bytes
$addr_byte = explode('-', $macAddress);
$hw_addr = "";
// Convert MAC-address from bytes to hexadecimal to decimal
for ($a=0; $a<6; $a++) $hw_addr .= chr(hexdec($addr_byte[$a]));
$hw_addr_string = "";
for ($a=0; $a<16; $a++) $hw_addr_string .= $hw_addr;
$buf .= $hw_addr_string;
}
if ($secureOn != "")
{
// Check whether $secureon is valid
$secureOn = strtoupper($secureOn);
$secureOn = str_replace(":", "-", $secureOn);
if ((!preg_match("/([A-F0-9]{2}[-]){5}([0-9A-F]){2}/", $secureOn)) || (strlen($secureOn) != 17))
{
throw new \Exception(__('Pattern of SecureOn-password is not "xx-xx-xx-xx-xx-xx" (x = digit or CAPITAL letter)'));
}
else
{
// Prepare magic packet: part 3/3 (Secureon password)
// Split MAC-address into an array of (six) bytes
$addr_byte = explode('-', $secureOn);
$hw_addr = "";
// Convert MAC address from hexadecimal to decimal
for ($a=0; $a<6; $a++) $hw_addr .= chr(hexdec($addr_byte[$a]));
$buf .= $hw_addr;
}
}
// Fill $addr with client's IP address, if $addr is empty
if ($address == "")
throw new \Exception(__('No IP Address Specified'));
// Resolve broadcast address
if (filter_var ($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) // same as (but easier than): preg_match("/\b(([01]?\d?\d|2[0-4]\d|25[0-5])\.){3}([01]?\d?\d|2[0-4]\d|25[0-5])\b/",$addr)
{
// $addr has an IP-adres format
}
else
{
throw new \Exception(__('IP Address Incorrectly Formed'));
}
// If $cidr is set, replace $addr for its broadcast address
if ($cidr != "")
{
// Check whether $cidr is valid
if ((!ctype_digit($cidr)) || ($cidr < 0) || ($cidr > 32))
{
throw new \Exception(__('CIDR subnet mask is not a number within the range of 0 till 32.'));
}
// Convert $cidr from one decimal to one inverted binary array
$inverted_binary_cidr = "";
// Build $inverted_binary_cidr by $cidr * zeros (this is the mask)
for ($a=0; $a<$cidr; $a++) $inverted_binary_cidr .= "0";
// Invert the mask (by postfixing ones to $inverted_binary_cidr untill 32 bits are filled/ complete)
$inverted_binary_cidr = $inverted_binary_cidr.substr("11111111111111111111111111111111", 0, 32 - strlen($inverted_binary_cidr));
// Convert $inverted_binary_cidr to an array of bits
$inverted_binary_cidr_array = str_split($inverted_binary_cidr);
// Convert IP address from four decimals to one binary array
// Split IP address into an array of (four) decimals
$addr_byte = explode('.', $address);
$binary_addr = "";
for ($a=0; $a<4; $a++)
{
// Prefix zeros
$pre = substr("00000000",0,8-strlen(decbin($addr_byte[$a])));
// Postfix binary decimal
$post = decbin($addr_byte[$a]);
$binary_addr .= $pre.$post;
}
// Convert $binary_addr to an array of bits
$binary_addr_array = str_split($binary_addr);
// Perform a bitwise OR operation on arrays ($binary_addr_array & $inverted_binary_cidr_array)
$binary_broadcast_addr_array="";
// binary array of 32 bit variables ('|' = logical operator 'or')
for ($a=0; $a<32; $a++) $binary_broadcast_addr_array[$a] = ($binary_addr_array[$a] | $inverted_binary_cidr_array[$a]);
// build binary address of four bundles of 8 bits (= 1 byte)
$binary_broadcast_addr = chunk_split(implode("", $binary_broadcast_addr_array), 8, ".");
// chop off last dot ('.')
$binary_broadcast_addr = substr($binary_broadcast_addr,0,strlen($binary_broadcast_addr)-1);
// binary array of 4 byte variables
$binary_broadcast_addr_array = explode(".", $binary_broadcast_addr);
$broadcast_addr_array = "";
// decimal array of 4 byte variables
for ($a=0; $a<4; $a++) $broadcast_addr_array[$a] = bindec($binary_broadcast_addr_array[$a]);
// broadcast address
$address = implode(".", $broadcast_addr_array);
}
// Check whether $port is valid
if ((!ctype_digit($port)) || ($port < 0) || ($port > 65536))
throw new \Exception(__('Port is not a number within the range of 0 till 65536. Port Provided: ' . $port));
// Check whether UDP is supported
if (!array_search('udp', stream_get_transports()))
throw new \Exception(__('No magic packet can been sent, since UDP is unsupported (not a registered socket transport)'));
// Ready to send the packet
if (function_exists('fsockopen'))
{
// Try fsockopen function - To do: handle error 'Permission denied'
$socket = fsockopen("udp://" . $address, $port, $errno, $errstr);
if ($socket)
{
$socket_data = fwrite($socket, $buf);
if ($socket_data)
{
$function = "fwrite";
$sent_fsockopen = "A magic packet of ".$socket_data." bytes has been sent via UDP to IP address: ".$address.":".$port.", using the '".$function."()' function.";
$content = bin2hex($buf);
$sent_fsockopen = $sent_fsockopen."Contents of magic packet:".strlen($content)." ".$content;
fclose($socket);
unset($socket);
$logger->notice($sent_fsockopen, 'display', 'WakeOnLan');
return true;
}
else
{
unset($socket);
throw new \Exception(__('Using "fwrite()" failed, due to error: ' . $errstr. ' ("' . $errno . '")'));
}
}
else
{
unset($socket);
$logger->notice(__('Using fsockopen() failed, due to denied permission'));
}
}
// Try socket_create function
if (function_exists('socket_create'))
{
// create socket based on IPv4, datagram and UDP
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
if ($socket)
{
// to enable manipulation of options at the socket level (you may have to change this to 1)
$level = SOL_SOCKET;
// to enable permission to transmit broadcast datagrams on the socket (you may have to change this to 6)
$optname = SO_BROADCAST;
$optval = true;
$opt_returnvalue = socket_set_option($socket, $level, $optname, $optval);
if ($opt_returnvalue < 0)
{
throw new \Exception(__('Using "socket_set_option()" failed, due to error: ' . socket_strerror($opt_returnvalue)));
}
$flags = 0;
// To do: handle error 'Operation not permitted'
$socket_data = socket_sendto($socket, $buf, strlen($buf), $flags, $address, $port);
if ($socket_data)
{
$function = "socket_sendto";
$socket_create = "A magic packet of ". $socket_data . " bytes has been sent via UDP to IP address: ".$address.":".$port.", using the '".$function."()' function.<br>";
$content = bin2hex($buf);
$socket_create = $socket_create . "Contents of magic packet:" . strlen($content) ." " . $content;
socket_close($socket);
unset($socket);
$logger->notice($socket_create, 'display', 'WakeOnLan');
return true;
}
else
{
$error = __('Using "socket_sendto()" failed, due to error: ' . socket_strerror(socket_last_error($socket)) . ' (' . socket_last_error($socket) . ')');
socket_close($socket);
unset($socket);
throw new \Exception($error);
}
}
else
{
throw new \Exception(__('Using "socket_sendto()" failed, due to error: ' . socket_strerror(socket_last_error($socket)) . ' (' . socket_last_error($socket) . ')'));
}
}
else
{
throw new \Exception(__('Wake On Lan Failed as there are no functions available to transmit it'));
}
}
}

View File

@@ -0,0 +1,567 @@
<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Helper;
use Exception;
use Xibo\Entity\Layout;
use Xibo\Entity\Permission;
use Xibo\Event\LibraryReplaceEvent;
use Xibo\Event\LibraryReplaceWidgetEvent;
use Xibo\Event\LibraryUploadCompleteEvent;
use Xibo\Event\MediaDeleteEvent;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\LibraryFullException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class XiboUploadHandler
* @package Xibo\Helper
*/
class XiboUploadHandler extends BlueImpUploadHandler
{
/**
* Handle form data from BlueImp
* @param $file
* @param $index
*/
protected function handleFormData($file, $index)
{
$controller = $this->options['controller'];
/* @var \Xibo\Controller\Library $controller */
// Handle form data, e.g. $_REQUEST['description'][$index]
// Link the file to the module
$fileName = $file->name;
$filePath = $controller->getConfig()->getSetting('LIBRARY_LOCATION') . 'temp/' . $fileName;
$this->getLogger()->debug('Upload complete for name: ' . $fileName . '. Index is ' . $index);
// Upload and Save
try {
// Check Library
if ($this->options['libraryQuotaFull']) {
throw new LibraryFullException(
sprintf(
__('Your library is full. Library Limit: %s K'),
$this->options['libraryLimit']
)
);
}
// Check for a user quota
// this method has the ability to reconnect to MySQL in the event that the upload has taken a long time.
// OSX-381
$controller->getUser()->isQuotaFullByUser(true);
// Get some parameters
$name = htmlspecialchars($this->getParam($index, 'name', $fileName));
$tags = $controller->getUser()->featureEnabled('tag.tagging')
? htmlspecialchars($this->getParam($index, 'tags', ''))
: '';
// Guess the type
$module = $controller->getModuleFactory()
->getByExtension(strtolower(substr(strrchr($fileName, '.'), 1)));
$this->getLogger()->debug(sprintf(
'Module Type = %s, Name = %s',
$module->type,
$module->name
));
// If we have an oldMediaId then we are replacing that media with new one
if ($this->options['oldMediaId'] != 0) {
$updateInLayouts = ($this->options['updateInLayouts'] == 1);
$deleteOldRevisions = ($this->options['deleteOldRevisions'] == 1);
$this->getLogger()->debug(sprintf(
'Replacing old with new - updateInLayouts = %d, deleteOldRevisions = %d',
$updateInLayouts,
$deleteOldRevisions
));
// Load old media
$oldMedia = $controller->getMediaFactory()->getById($this->options['oldMediaId']);
// Check permissions
if (!$controller->getUser()->checkEditable($oldMedia)) {
throw new AccessDeniedException(__('Access denied replacing old media'));
}
// Check to see if we are changing the media type
if ($oldMedia->mediaType != $module->type && $this->options['allowMediaTypeChange'] == 0) {
throw new InvalidArgumentException(
__('You cannot replace this media with an item of a different type')
);
}
// Set the old record to edited
$oldMedia->isEdited = 1;
$oldMedia->save(['validate' => false]);
// The media name might be empty here, because the user isn't forced to select it
$name = ($name == '') ? $oldMedia->name : $name;
$tags = ($tags == '') ? '' : $tags;
// Add the Media
// the userId is either the existing user
// (if we are changing media type) or the currently logged-in user otherwise.
$media = $controller->getMediaFactory()->create(
$name,
$fileName,
$module->type,
$oldMedia->getOwnerId()
);
if ($tags != '') {
$concatTags = $oldMedia->getTagString() . ',' . $tags;
$media->updateTagLinks($controller->getTagFactory()->tagsFromString($concatTags));
}
// Apply the duration from the old media, unless we're a video
if ($module->type === 'video') {
$media->duration = $module->fetchDurationOrDefaultFromFile($filePath);
} else {
$media->duration = $oldMedia->duration;
}
// Raise an event for this media item
$controller->getDispatcher()->dispatch(
new LibraryReplaceEvent($module, $media, $oldMedia),
LibraryReplaceEvent::$NAME
);
$media->enableStat = $oldMedia->enableStat;
$media->expires = $this->options['expires'];
$media->folderId = $this->options['oldFolderId'];
$media->permissionsFolderId = $oldMedia->permissionsFolderId;
// Save
$media->save(['oldMedia' => $oldMedia]);
// Upload finished
$controller->getDispatcher()->dispatch(
new LibraryUploadCompleteEvent($media),
LibraryUploadCompleteEvent::$NAME
);
$this->getLogger()->debug('Copying permissions to new media');
foreach ($controller->getPermissionFactory()->getAllByObjectId(
$controller->getUser(),
get_class($oldMedia),
$oldMedia->mediaId
) as $permission) {
/* @var Permission $permission */
$permission = clone $permission;
$permission->objectId = $media->mediaId;
$permission->save();
}
// Do we want to replace this in all layouts?
if ($updateInLayouts) {
$this->getLogger()->debug('Replace in all Layouts selected. Getting associated widgets');
foreach ($controller->getWidgetFactory()->getByMediaId($oldMedia->mediaId, 0) as $widget) {
$this->getLogger()->debug('Found widgetId ' . $widget->widgetId
. ' to assess, type is ' . $widget->type);
if (!$controller->getUser()->checkEditable($widget)) {
// Widget that we cannot update,
// this means we can't delete the original mediaId when it comes time to do so.
$deleteOldRevisions = false;
$controller
->getLog()->info('Media used on Widget that we cannot edit. Delete Old Revisions has been disabled.'); //phpcs:ignore
}
// Load the module for this widget.
$moduleToReplace = $controller->getModuleFactory()->getByType($widget->type);
// If we are replacing an audio media item,
// we should check to see if the widget we've found has any
// audio items assigned.
if ($module->type == 'audio'
&& in_array($oldMedia->mediaId, $widget->getAudioIds())
) {
$this->getLogger()->debug('Found audio on widget that needs updating. widgetId = ' .
$widget->getId() . '. Linking ' . $media->mediaId);
$widget->unassignAudioById($oldMedia->mediaId);
$widget->assignAudioById($media->mediaId);
$widget->save();
} else if ($widget->type !== 'global'
&& count($widget->getPrimaryMedia()) > 0
&& $widget->getPrimaryMediaId() == $oldMedia->mediaId
) {
// We're only interested in primary media at this point (no audio)
// Check whether this widget is of the same type as our incoming media item
// This needs to be applicable only to non region specific Widgets,
// otherwise we would not be able to replace Media references in region specific Widgets.
// If these types are different, and the module we're replacing isn't region specific
// then we need to see if we're allowed to change it.
if ($widget->type != $module->type && $moduleToReplace->regionSpecific == 0) {
// Are we supposed to switch, or should we prevent?
if ($this->options['allowMediaTypeChange'] == 1) {
$widget->type = $module->type;
} else {
throw new InvalidArgumentException(__(
'You cannot replace this media with an item of a different type'
));
}
}
$this->getLogger()->debug(sprintf(
'Found widget that needs updating. ID = %d. Linking %d',
$widget->getId(),
$media->mediaId
));
$widget->unassignMedia($oldMedia->mediaId);
$widget->assignMedia($media->mediaId);
// calculate duration
$widget->calculateDuration($module);
// replace mediaId references in applicable widgets
$controller->getLayoutFactory()->handleWidgetMediaIdReferences(
$widget,
$media->mediaId,
$oldMedia->mediaId
);
// Raise an event for this media item
$controller->getDispatcher()->dispatch(
new LibraryReplaceWidgetEvent($module, $widget, $media, $oldMedia),
LibraryReplaceWidgetEvent::$NAME
);
// Save
$widget->save(['alwaysUpdate' => true]);
}
// Does this widget have any elements?
if ($moduleToReplace->regionSpecific == 1) {
// This is a global widget and will have elements which refer to this media id.
$this->getLogger()
->debug('handleFormData: This is a region specific widget, checking for elements.');
// We need to load options as that is where we store elements
$widget->load(false);
// Parse existing elements.
$mediaFoundInElement = false;
$elements = json_decode($widget->getOptionValue('elements', '[]'), true);
foreach ($elements as $index => $widgetElement) {
foreach ($widgetElement['elements'] ?? [] as $elementIndex => $element) {
// mediaId on the element, used for things like image element
if (!empty($element['mediaId']) && $element['mediaId'] == $oldMedia->mediaId) {
// We have found an element which uses the mediaId we are replacing
$elements[$index]['elements'][$elementIndex]['mediaId'] = $media->mediaId;
// Swap the ID on the link record
$widget->unassignMedia($oldMedia->mediaId);
$widget->assignMedia($media->mediaId);
$mediaFoundInElement = true;
}
// mediaId on the property, used for mediaSelector properties.
foreach ($element['properties'] ?? [] as $propertyIndex => $property) {
if (!empty($property['mediaId'])) {
// TODO: should we really load in all templates here and replace?
// Set the mediaId and value of this property
// this only works because mediaSelector is the only property which
// uses mediaId and it always has the value set.
$elements[$index]['elements'][$elementIndex]['properties']
[$propertyIndex]['mediaId'] = $media->mediaId;
$elements[$index]['elements'][$elementIndex]['properties']
[$propertyIndex]['value'] = $media->mediaId;
$widget->unassignMedia($oldMedia->mediaId);
$widget->assignMedia($media->mediaId);
$mediaFoundInElement = true;
}
}
}
}
if ($mediaFoundInElement) {
$this->getLogger()
->debug('handleFormData: mediaId found in elements, replacing');
// Save the new elements
$widget->setOptionValue('elements', 'raw', json_encode($elements));
// Raise an event for this media item
$controller->getDispatcher()->dispatch(
new LibraryReplaceWidgetEvent($module, $widget, $media, $oldMedia),
LibraryReplaceWidgetEvent::$NAME
);
// Save
$widget->save(['alwaysUpdate' => true]);
}
}
}
// Update any background images
if ($media->mediaType == 'image') {
$this->getLogger()->debug(sprintf(
'Updating layouts with the old media %d as the background image.',
$oldMedia->mediaId
));
// Get all Layouts with this as the background image
foreach ($controller->getLayoutFactory()->query(
null,
['disableUserCheck' => 1, 'backgroundImageId' => $oldMedia->mediaId]
) as $layout) {
/* @var Layout $layout */
if (!$controller->getUser()->checkEditable($layout)) {
// Widget that we cannot update,
// this means we can't delete the original mediaId when it comes time to do so.
$deleteOldRevisions = false;
$this->getLogger()->info(
'Media used on Widget that we cannot edit. Delete Old Revisions has been disabled.'
);
}
$this->getLogger()->debug(sprintf(
'Found layout that needs updating. ID = %d. Setting background image id to %d',
$layout->layoutId,
$media->mediaId
));
$layout->backgroundImageId = $media->mediaId;
$layout->save();
}
}
} elseif ($this->options['widgetId'] != 0) {
$this->getLogger()->debug('Swapping a specific widget only.');
// swap this one
$widget = $controller->getWidgetFactory()->getById($this->options['widgetId']);
if (!$controller->getUser()->checkEditable($widget)) {
throw new AccessDeniedException();
}
$widget->unassignMedia($oldMedia->mediaId);
$widget->assignMedia($media->mediaId);
$widget->save();
}
// We either want to Link the old record to this one, or delete it
if ($updateInLayouts && $deleteOldRevisions) {
$this->getLogger()->debug('Delete old revisions of ' . $oldMedia->mediaId);
// Check we have permission to delete this media
if (!$controller->getUser()->checkDeleteable($oldMedia)) {
throw new AccessDeniedException(
__('You do not have permission to delete the old version.')
);
}
try {
// Join the prior revision up with the new media.
$priorMedia = $controller->getMediaFactory()->getParentById($oldMedia->mediaId);
$this->getLogger()->debug(
'Prior media found, joining ' .
$priorMedia->mediaId . ' with ' . $media->mediaId
);
$priorMedia->parentId = $media->mediaId;
$priorMedia->save(['validate' => false]);
} catch (NotFoundException $e) {
// Nothing to do then
$this->getLogger()->debug('No prior media found');
}
$controller->getDispatcher()->dispatch(
new MediaDeleteEvent($oldMedia),
MediaDeleteEvent::$NAME
);
$oldMedia->delete();
} else {
$oldMedia->parentId = $media->mediaId;
$oldMedia->save(['validate' => false]);
}
} else {
// Not a replacement
// Fresh upload
// The media name might be empty here, because the user isn't forced to select it
$name = ($name == '') ? $fileName : $name;
$tags = ($tags == '') ? '' : $tags;
// Add the Media
$media = $controller->getMediaFactory()->create(
$name,
$fileName,
$module->type,
$this->options['userId']
);
if ($tags != '') {
$media->updateTagLinks($controller->getTagFactory()->tagsFromString($tags));
}
// Set the duration
$media->duration = $module->fetchDurationOrDefaultFromFile($filePath);
if ($media->enableStat == null) {
$media->enableStat = $controller->getConfig()->getSetting('MEDIA_STATS_ENABLED_DEFAULT');
}
// Media library expiry.
$media->expires = $this->options['expires'];
$media->folderId = $this->options['oldFolderId'];
// Permissions
$folder = $controller->getFolderFactory()->getById($this->options['oldFolderId'], 0);
$media->permissionsFolderId = $folder->getPermissionFolderIdOrThis();
// Save
$media->save();
// Upload finished
$controller->getDispatcher()->dispatch(
new LibraryUploadCompleteEvent($media),
LibraryUploadCompleteEvent::$NAME
);
}
// Configure the return values according to the media item we've added
$file->name = $name;
$file->mediaId = $media->mediaId;
$file->storedas = $media->storedAs;
$file->duration = $media->duration;
$file->retired = $media->retired;
$file->fileSize = $media->fileSize;
$file->md5 = $media->md5;
$file->enableStat = $media->enableStat;
$file->width = $media->width;
$file->height = $media->height;
$file->mediaType = $module->type;
$file->fileName = $fileName;
// Test to ensure the final file size is the same as the file size we're expecting
if ($file->fileSize != $file->size) {
throw new InvalidArgumentException(
__('Sorry this is a corrupted upload, the file size doesn\'t match what we\'re expecting.'),
'size'
);
}
// Are we assigning to a Playlist?
if ($this->options['playlistId'] != 0 && $this->options['widgetId'] == 0) {
$this->getLogger()->debug('Assigning uploaded media to playlistId '
. $this->options['playlistId']);
// Get the Playlist
$playlist = $controller->getPlaylistFactory()->getById($this->options['playlistId']);
if (!$playlist->isEditable()) {
throw new InvalidArgumentException(
__('This Layout is not a Draft, please checkout.'),
'layoutId'
);
}
// Create a Widget and add it to our region
$widget = $controller->getWidgetFactory()->create(
$this->options['userId'],
$playlist->playlistId,
$module->type,
$media->duration,
$module->schemaVersion
);
// Default options
$widget->setOptionValue(
'enableStat',
'attrib',
$controller->getConfig()->getSetting('WIDGET_STATS_ENABLED_DEFAULT')
);
// From/To dates?
$widget->fromDt = $this->options['widgetFromDt'];
$widget->toDt = $this->options['widgetToDt'];
$widget->setOptionValue('deleteOnExpiry', 'attrib', $this->options['deleteOnExpiry']);
// Assign media
$widget->assignMedia($media->mediaId);
// Calculate the widget duration for new uploaded media widgets
$widget->calculateDuration($module);
// Assign the new widget to the playlist
$playlist->assignWidget($widget, $this->options['displayOrder'] ?? null);
// Save the playlist
$playlist->save();
// Configure widgetId is response
$file->widgetId = $widget->widgetId;
}
} catch (Exception $e) {
$this->getLogger()->error('Error uploading media: ' . $e->getMessage());
$this->getLogger()->debug($e->getTraceAsString());
// Unlink the temporary file
@unlink($filePath);
$file->error = $e->getMessage();
// Don't commit
$controller->getState()->setCommitState(false);
}
}
/**
* Get Param from File Input, taking into account multi-upload index if applicable
* @param int $index
* @param string $param
* @param mixed $default
* @return mixed
*/
private function getParam($index, $param, $default)
{
if ($index === null) {
if (isset($_REQUEST[$param])) {
return $_REQUEST[$param];
} else {
return $default;
}
} else {
if (isset($_REQUEST[$param][$index])) {
return $_REQUEST[$param][$index];
} else {
return $default;
}
}
}
}