<?php
namespace Jodit;
defined('_ELXIS_') or die ('Direct access to this location is not allowed');
use abeautifulsite\SimpleImage;
abstract class Application {
/**
* @var Config
*/
public $config;
/**
* Check whether the user has the ability to view files
* You can define JoditCheckPermissions function in config.php and use it
*/
abstract public function checkAuthentication ();
function corsHeaders() {
if (isset($_SERVER['HTTP_ORIGIN'])) {
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
} else {
header("Access-Control-Allow-Origin: *");
}
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Headers: Origin,X-Requested-With,Content-Type,Accept');
header('Access-Control-Max-Age: 86400'); // cache for 1 day
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
}
exit(0);
}
}
function display () {
if (!$this->config->debug) {
ob_end_clean();
header('Content-Type: application/json');
}
// replace full path from message
foreach ($this->config->sources as $source) {
if (isset($this->response->data->messages)) {
foreach ($this->response->data->messages as &$message) {
$message = str_replace($source->root, '/', $message);
}
}
}
exit(json_encode($this->response, $this->config->debug ? JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES: 0));
}
/**
* Get user role
*
* @return string
*/
function getUserRole() {
return isset($_SESSION[$this->config->roleSessionVar]) ? $_SESSION[$this->config->roleSessionVar] : $this->config->defaultRole;
}
function execute () {
$methods = get_class_methods($this);
if (in_array('action' . ucfirst($this->action), $methods)) {
$this->accessControl->checkPermission($this->getUserRole(), $this->action);
$this->response->data = (object)call_user_func_array([$this, 'action' . $this->action], []);
} else {
throw new \Exception('Action "' . $this->action . '" not found', 404);
}
$this->response->success = true;
$this->response->data->code = 220;
$this->display();
}
/**
* Constructor FileBrowser
*
* @param {array} $config
* @throws \Exception
*/
/**
* @var \Jodit\AccessControl
*/
public $accessControl;
function __construct ($config) {
ob_start();
// set_error_handler([$this, 'errorHandler'], E_ALL);
set_exception_handler([$this, 'exceptionHandler']);
$this->config = new Config($config);
if ($this->config->allowCrossOrigin) {
$this->corsHeaders();
}
$this->response = new Response();
$this->request = new Request();
$this->action = $this->request->action;
if ($this->config->debug) {
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
ini_set('display_errors', 'on');
} else {
error_reporting(0);
ini_set('display_errors', 'off');
}
if ($this->request->source && $this->request->source !== 'default' && empty($this->config->sources[$this->request->source])) {
throw new \Exception('Need valid parameter source key', 400);
}
$this->accessControl = new AccessControl();
}
/**
* Get default(first) source or by $_REQUEST['source']
*
* @return Source
*/
public function getSource() {
if (!$this->request->source || empty($this->config->sources[$this->request->source])) {
foreach ($this->config->sources as $k => $src) { return $src; }
}
return $this->config->sources[$this->request->source];
}
/**
* @property Response $response
*/
public $response;
/**
* @property Request $request
*/
public $request;
/**
* @property string $action
*/
public $action;
/**
* Check file extension
*
* @param {string} $file
* @param {Source} $source
* @return bool
*/
private function isGoodFile($file, Source $source) {
$info = pathinfo($file);
if (!isset($info['extension']) or (!in_array(strtolower($info['extension']), $source->extensions))) {
return false;
}
if (in_array(strtolower($info['extension']), $source->imageExtensions) and !Helper::isImage($file)) {
return false;
}
return true;
}
protected function getImageEditorInfo() {
$source = $this->getSource();
$path = $source->getPath();
$file = $this->request->name;
$box = (object)[
'w' => 0,
'h' => 0,
'x' => 0,
'y' => 0,
];
if ($this->request->box && is_array($this->request->box)) {
foreach ($box as $key=>&$value) {
$value = isset($this->request->box[$key]) ? $this->request->box[$key] : 0;
}
}
$newName = $this->request->newname ? Helper::makeSafe($this->request->newname) : '';
if (!$path || !$file || !file_exists($path . $file) || !is_file($path . $file)) {
throw new \Exception('Source file not set or not exists', 404);
}
$img = new SimpleImage();
$img->load($path . $file);
if ($newName) {
$info = pathinfo($path . $file);
// if has not same extension
if (!preg_match('#\.(' . $info['extension'] . ')$#i', $newName)) {
$newName = $newName . '.' . $info['extension'];
}
if (!$this->config->allowReplaceSourceFile and file_exists($path . $newName)) {
throw new \Exception('File ' . $newName . ' already exists', 400);
}
} else {
$newName = $file;
}
if (file_exists($path . $this->config->thumbFolderName . DIRECTORY_SEPARATOR . $newName)) {
unlink($path . $this->config->thumbFolderName . DIRECTORY_SEPARATOR . $newName);
}
$info = $img->get_original_info();
return (object)[
'path' => $path,
'file' => $file,
'box' => $box,
'newname' => $newName,
'img' => $img,
'width' => $info['width'],
'height' => $info['height'],
];
}
/**
* Error handler
*
* @param {int} errno contains the level of the error raised, as an integer.
* @param {string} errstr contains the error message, as a string.
* @param {string} errfile which contains the filename that the error was raised in, as a string.
* @param {string} errline which contains the line number the error was raised at, as an integer.
*/
public function errorHandler ($errorNumber, $errorMessage, $file, $line) {
$this->response->success = false;
$this->response->data->code = $errorNumber;
$this->response->data->messages[] = $errorMessage . ($this->config->debug ? ' - file:' . $file . ' line:' . $line : '');
$this->display();
}
/**
* @param \Exception $exception
*/
public function exceptionHandler ($exception) {
$this->errorHandler($exception->getCode(), $exception->getMessage(), $exception->getFile(), $exception->getLine());
}
/**
* Load all files from folder ore source or sources
*/
protected function actionFiles() {
$sources = [];
foreach ($this->config->sources as $key => $source) {
if ($this->request->source && $this->request->source !== 'default' && $key !== $this->request->source && $this->request->path !== './') {
continue;
}
$path = $source->getPath();
try {
$this->accessControl->checkPermission($this->getUserRole(), $this->action, $path);
} catch (\Exception $e) {
continue;
}
$sourceData = (object)[
'baseurl' => $source->baseurl,
'path' => str_replace(realpath($source->getRoot()) . DIRECTORY_SEPARATOR, '', $path),
'files' => [],
];
$dir = opendir($path);
while ($file = readdir($dir)) {
if ($file != '.' && $file != '..' && is_file($path . $file)) {
if ($this->isGoodFile($path . $file, $source)) {
$item = [
'file' => $file,
];
if ($this->config->createThumb) {
if (!is_dir($path . $this->config->thumbFolderName)) {
mkdir($path . $this->config->thumbFolderName, 0777);
}
if (!file_exists($path . $this->config->thumbFolderName . DIRECTORY_SEPARATOR . $file)) {
try {
$img = new SimpleImage($path . $file);
$img
->best_fit(150, 150)
->save($path . $this->config->thumbFolderName . DIRECTORY_SEPARATOR . $file, $this->config->quality);
} catch (\Exception $e) {
continue;
}
}
$item['thumb'] = $this->config->thumbFolderName . DIRECTORY_SEPARATOR . $file;
}
$item['changed'] = date($this->config->datetimeFormat, filemtime($path.$file));
$item['size'] = Helper::humanFileSize(filesize($path.$file));
$sourceData->files[] = $item;
}
}
}
$sources[$key] = $sourceData;
}
return [
'sources' => $sources
];
}
/**
* Load all folders from folder ore source or sources
*/
protected function actionFolders() {
$sources = [];
foreach ($this->config->sources as $key => $source) {
if ($this->request->source && $this->request->source !== 'default' && $key !== $this->request->source && $this->request->path !== './') {
continue;
}
$path = $source->getPath();
try {
$this->accessControl->checkPermission($this->getUserRole(), $this->action, $path);
} catch (\Exception $e) {
continue;
}
$sourceData = (object)[
'baseurl' => $source->baseurl,
'path' => str_replace(realpath($source->getRoot()) . DIRECTORY_SEPARATOR, '', $path),
'folders' => [],
];
$sourceData->folders[] = $path == $source->getRoot() ? '.' : '..';
$dir = opendir($path);
while ($file = readdir($dir)) {
if ($file != '.' && $file != '..' && is_dir($path . $file) and (!$this->config->createThumb || $file !== $this->config->thumbFolderName) and !in_array($file, $this->config->excludeDirectoryNames)) {
$sourceData->folders[] = $file;
}
}
$sources[$key] = $sourceData;
}
return [
'sources' => $sources
];
}
/**
* Load remote image by URL to self host
* @throws \Exception
*/
protected function actionFileUploadRemote() {
$url = $this->request->url;
if (!$url) {
throw new \Exception('Need url parameter', 400);
}
$result = parse_url($url);
if (!isset($result['host']) || !isset($result['path'])) {
throw new \Exception('Not valid URL', 400);
}
$filename = Helper::makeSafe(basename($result['path']));
if (!$filename) {
throw new \Exception('Not valid URL', 400);
}
Helper::downloadRemoteFile($url, $this->getSource()->getRoot() . $filename);
$extension = pathinfo($this->getSource()->getRoot() . $filename, PATHINFO_EXTENSION);
try {
$this->accessControl->checkPermission($this->getUserRole(), $this->action, $this->getSource()->getRoot(), $extension);
} catch (\Exception $e) {
unlink($this->getSource()->getRoot() . $filename);
throw $e;
}
return [
'newfilename' => $filename,
'baseurl' => $this->getSource()->baseurl,
];
}
/**
* Upload images
*
* @return array
* @throws \Exception
*/
protected function actionFileUpload() {
$source = $this->getSource();
$root = $source->getRoot();
$path = $source->getPath();
$messages = [];
$files = [];
if (isset($_FILES['files']) and is_array($_FILES['files']) and isset($_FILES['files']['name']) and is_array($_FILES['files']['name']) and count($_FILES['files']['name'])) {
foreach ($_FILES['files']['name'] as $i => $file) {
if ($_FILES['files']['error'][$i]) {
throw new \Exception(isset(Helper::$upload_errors[$_FILES['files']['error'][$i]]) ? Helper::$upload_errors[$_FILES['files']['error'][$i]] : 'Error', $_FILES['files']['error'][$i]);
}
$tmp_name = $_FILES['files']['tmp_name'][$i];
if ($source->maxFileSize and filesize($tmp_name) > Helper::convertToBytes($source->maxFileSize)) {
unlink($tmp_name);
throw new \Exception('File size exceeds the allowable', 403);
}
if (move_uploaded_file($tmp_name, $file = $path . Helper::makeSafe($_FILES['files']['name'][$i]))) {
try {
$this->accessControl->checkPermission($this->getUserRole(), $this->action, $source->getRoot(), pathinfo($file, PATHINFO_EXTENSION));
} catch (\Exception $e) {
unlink($file);
throw $e;
}
if (!$this->isGoodFile($file, $source)) {
unlink($file);
throw new \Exception('File type is not in white list', 403);
}
$messages[] = 'File ' . $_FILES['files']['name'][$i] . ' was upload';
$files[] = str_replace($root, '', $file);
} else {
if (!is_writable($path)) {
throw new \Exception('Destination directory is not writeble', 424);
}
throw new \Exception('No files have been uploaded', 422);
}
}
}
if (!count($files)) {
throw new \Exception('No files have been uploaded', 422);
}
return [
'baseurl' => $source->baseurl,
'messages' => $messages,
'files' => $files
];
}
/**
* Remove file
*
* @throws \Exception
*/
protected function actionFileRemove() {
$source = $this->getSource();
$file_path = false;
$path = $source->getPath();
$this->accessControl->checkPermission($this->getUserRole(), $this->action, $path);
$target = $this->request->name;
if (realpath($path . $target) && strpos(realpath($path . $target), $source->getRoot()) !== false) {
$file_path = realpath($path . $target);
}
if ($file_path && file_exists($file_path)) {
if (is_file($file_path)) {
$result = unlink($file_path);
if ($result) {
$file = basename($file_path);
$thumb = dirname($file_path) . DIRECTORY_SEPARATOR . $source->thumbFolderName . DIRECTORY_SEPARATOR . $file;
if (file_exists($thumb)) {
unlink($thumb);
if (!count(glob(dirname($thumb) . DIRECTORY_SEPARATOR . "*"))) {
rmdir(dirname($thumb));
}
}
} else {
$error = (object)error_get_last();
throw new \Exception('Delete failed! ' . $error->message, 424);
}
} else {
throw new \Exception('It is not a file!', 424);
}
} else {
throw new \Exception('File or directory not exists' . $path . $target, 400);
}
}
/**
* Remove folder
*
* @throws \Exception
*/
protected function actionFolderRemove() {
$source = $this->getSource();
$file_path = false;
$path = $source->getPath();
$this->accessControl->checkPermission($this->getUserRole(), $this->action, $path);
$target = $this->request->name;
if (realpath($path . $target) && strpos(realpath($path . $target), $source->getRoot()) !== false) {
$file_path = realpath($path . $target);
}
if ($file_path && file_exists($file_path)) {
if (is_dir($file_path)) {
$thumb = $file_path . DIRECTORY_SEPARATOR . $source->thumbFolderName . DIRECTORY_SEPARATOR;
if (is_dir($thumb)) {
Helper::deleteDir($thumb);
}
Helper::deleteDir($file_path);
} else {
throw new \Exception('It is not a directory!', 424);
}
} else {
throw new \Exception('Directory not exists', 400);
}
}
/**
* Create directory
* @throws \Exception
*/
protected function actionFolderCreate() {
$source = $this->getSource();
$destinationPath = $source->getPath();
$this->accessControl->checkPermission($this->getUserRole(), $this->action, $destinationPath);
$folderName = Helper::makeSafe($this->request->name);
if ($destinationPath) {
if ($folderName) {
if (!realpath($destinationPath . $folderName)) {
mkdir($destinationPath . $folderName, $source->defaultPermission);
if (is_dir($destinationPath . $folderName)) {
return ['messages' => ['Directory successfully created']];
}
throw new \Exception('Directory was not created', 404);
}
throw new \Exception('Directory already exists', 406);
}
throw new \Exception('The name for new directory has not been set', 406);
}
throw new \Exception('The destination directory has not been set', 406);
}
/**
* Move file or directory to another folder
*
* @throws \Exception
*/
protected function actionMove() {
$source = $this->getSource();
$destinationPath = $source->getPath();
$sourcePath = $source->getPath($this->request->from);
$this->accessControl->checkPermission($this->getUserRole(), $this->action, $destinationPath);
$this->accessControl->checkPermission($this->getUserRole(), $this->action, $sourcePath);
if ($sourcePath) {
if ($destinationPath) {
if (is_file($sourcePath) or is_dir($sourcePath)) {
rename($sourcePath, $destinationPath . basename($sourcePath));
} else {
throw new \Exception('Not file', 404);
}
} else {
throw new \Exception('Need destination path', 400);
}
} else {
throw new \Exception('Need source path', 400);
}
}
/**
* Resize image
*
* @throws \Exception
*/
protected function actionImageResize() {
$source = $this->getSource();
$this->accessControl->checkPermission($this->getUserRole(), $this->action, $source->getPath());
$info = $this->getImageEditorInfo();
if (!$info->box || (int)$info->box->w <= 0) {
throw new \Exception('Width not specified', 400);
}
if (!$info->box || (int)$info->box->h <= 0) {
throw new \Exception('Height not specified', 400);
}
$info->img
->resize((int)$info->box->w, (int)$info->box->h)
->save($info->path . $info->newname, $source->quality);
}
protected function actionImageCrop() {
$source = $this->getSource();
$this->accessControl->checkPermission($this->getUserRole(), $this->action, $source->getPath());
$info = $this->getImageEditorInfo();
if ((int)$info->box->x < 0 || (int)$info->box->x > (int)$info->width) {
throw new \Exception('Start X not specified', 400);
}
if ((int)$info->box->y < 0 || (int)$info->box->y > (int)$info->height) {
throw new \Exception('Start Y not specified', 400);
}
if ((int)$info->box->w <= 0) {
throw new \Exception('Width not specified', 400);
}
if ((int)$info->box->h <= 0) {
throw new \Exception('Height not specified', 400);
}
$info->img
->crop((int)$info->box->x, (int)$info->box->y, (int)$info->box->x + (int)$info->box->w, (int)$info->box->y + (int)$info->box->h)
->save($info->path . $info->newname, $source->quality);
}
/**
* Get filepath by URL for local files
*
* @metod actionGetFileByURL
*/
function actionGetLocalFileByUrl() {
$url = $this->request->url;
if (!$url) {
throw new \Exception('Need full url', 400);
}
$parts = parse_url($url);
if (empty($parts['path'])) {
throw new \Exception('Empty url', 400);
}
$found = false;
$path = '';
$root = '';
$key = 0;
foreach ($this->config->sources as $key => $source) {
if ($this->request->source && $this->request->source !== 'default' && $key !== $this->request->source && $this->request->path !== './') {
continue;
}
$base = parse_url($source->baseurl);
$path = preg_replace('#^(/)?' . $base['path'] . '#', '', $parts['path']);
$root = $source->getPath();
if (file_exists($root . $path) && is_file($root . $path) && $this->isGoodFile($root . $path, $source)) {
$found = true;
break;
}
}
if (!$found) {
throw new \Exception('File does not exist or is above the root of the connector', 424);
}
return [
'path' => str_replace($root, '', dirname($root . $path) . DIRECTORY_SEPARATOR),
'name' => basename($path),
'source' => $key
];
}
}