init commit
This commit is contained in:
188
lib/Helper/ApplicationState.php
Normal file
188
lib/Helper/ApplicationState.php
Normal 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;
|
||||
}
|
||||
}
|
||||
42
lib/Helper/AttachmentUploadHandler.php
Normal file
42
lib/Helper/AttachmentUploadHandler.php
Normal 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);
|
||||
}
|
||||
}
|
||||
500
lib/Helper/BlueImpUploadHandler.php
Normal file
500
lib/Helper/BlueImpUploadHandler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
68
lib/Helper/ByteFormatter.php
Normal file
68
lib/Helper/ByteFormatter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
176
lib/Helper/DataSetUploadHandler.php
Normal file
176
lib/Helper/DataSetUploadHandler.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
186
lib/Helper/DatabaseLogHandler.php
Normal file
186
lib/Helper/DatabaseLogHandler.php
Normal 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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
265
lib/Helper/DateFormatHelper.php
Normal file
265
lib/Helper/DateFormatHelper.php
Normal 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
356
lib/Helper/Environment.php
Normal 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');
|
||||
}
|
||||
}
|
||||
122
lib/Helper/HttpCacheProvider.php
Normal file
122
lib/Helper/HttpCacheProvider.php
Normal 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
205
lib/Helper/HttpsDetect.php
Normal 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
576
lib/Helper/Install.php
Normal 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;
|
||||
}
|
||||
}
|
||||
143
lib/Helper/LayoutUploadHandler.php
Normal file
143
lib/Helper/LayoutUploadHandler.php
Normal 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
167
lib/Helper/LinkSigner.php
Normal 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;
|
||||
}
|
||||
}
|
||||
44
lib/Helper/LogoutTrait.php
Normal file
44
lib/Helper/LogoutTrait.php
Normal 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')
|
||||
]);
|
||||
}
|
||||
}
|
||||
62
lib/Helper/NatoAlphabet.php
Normal file
62
lib/Helper/NatoAlphabet.php
Normal 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);
|
||||
}
|
||||
}
|
||||
42
lib/Helper/NullHelpService.php
Normal file
42
lib/Helper/NullHelpService.php
Normal 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 = '')
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
41
lib/Helper/NullSanitizer.php
Normal file
41
lib/Helper/NullSanitizer.php
Normal 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()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
66
lib/Helper/NullSession.php
Normal file
66
lib/Helper/NullSession.php
Normal 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
36
lib/Helper/NullView.php
Normal 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
37
lib/Helper/ObjectVars.php
Normal 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
123
lib/Helper/Pbkdf2Hash.php
Normal 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
62
lib/Helper/Profiler.php
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
104
lib/Helper/QuickChartQRProvider.php
Normal file
104
lib/Helper/QuickChartQRProvider.php
Normal 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
51
lib/Helper/Random.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
lib/Helper/RouteLogProcessor.php
Normal file
54
lib/Helper/RouteLogProcessor.php
Normal 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;
|
||||
}
|
||||
}
|
||||
53
lib/Helper/SanitizerService.php
Normal file
53
lib/Helper/SanitizerService.php
Normal 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
67
lib/Helper/SendFile.php
Normal 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
547
lib/Helper/Session.php
Normal 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
35
lib/Helper/Status.php
Normal 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
238
lib/Helper/Translate.php
Normal 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;
|
||||
}
|
||||
}
|
||||
124
lib/Helper/UploadHandler.php
Normal file
124
lib/Helper/UploadHandler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
lib/Helper/UserLogProcessor.php
Normal file
63
lib/Helper/UserLogProcessor.php
Normal 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
271
lib/Helper/WakeOnLan.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
567
lib/Helper/XiboUploadHandler.php
Normal file
567
lib/Helper/XiboUploadHandler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user