mirror of
https://github.com/Studio-42/elFinder
synced 2026-06-08 12:37:09 +00:00
409 lines
9.6 KiB
PHP
409 lines
9.6 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Abstract class of editor plugins.
|
|
*
|
|
* @author Naoki Sawada
|
|
*/
|
|
class elFinderEditor
|
|
{
|
|
/**
|
|
* Lifetime of callback states.
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $callbackStateTtl = 86400;
|
|
|
|
/**
|
|
* Dedicated directory name for callback states.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $callbackStateDirName = 'elfinder_editor_callback_state';
|
|
|
|
/**
|
|
* Array of allowed method by request from client side.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $allowed = array();
|
|
|
|
/**
|
|
* elFinder instance
|
|
*
|
|
* @var object elFinder instance
|
|
*/
|
|
protected $elfinder;
|
|
|
|
/**
|
|
* Arguments
|
|
*
|
|
* @var array argValues
|
|
*/
|
|
protected $args;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param object $elfinder
|
|
* @param array $args
|
|
*/
|
|
public function __construct($elfinder, $args)
|
|
{
|
|
$this->elfinder = $elfinder;
|
|
$this->args = $args;
|
|
}
|
|
|
|
/**
|
|
* Return boolean that this plugin is enabled.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function enabled()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Return boolean that $name method is allowed.
|
|
*
|
|
* @param string $name
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isAllowedMethod($name)
|
|
{
|
|
$checker = array_flip($this->allowed);
|
|
|
|
return isset($checker[$name]);
|
|
}
|
|
|
|
/**
|
|
* Return $this->args value of the key
|
|
*
|
|
* @param string $key target key
|
|
* @param string $empty empty value
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function argValue($key, $empty = '')
|
|
{
|
|
return isset($this->args[$key]) ? $this->args[$key] : $empty;
|
|
}
|
|
|
|
/**
|
|
* Create callback state and return token params for a callback request.
|
|
*
|
|
* @param string $method
|
|
* @param string $hash
|
|
* @param string $secret
|
|
* @param array $meta
|
|
* @param int $ttl
|
|
*
|
|
* @return array|false
|
|
*/
|
|
protected function createCallbackState($method, $hash, $secret, $meta = array(), $ttl = null)
|
|
{
|
|
$this->gcCallbackStates();
|
|
|
|
$token = $this->createCallbackStateToken();
|
|
$expires = time() + max(60, is_null($ttl) ? (int)$this->callbackStateTtl : (int)$ttl);
|
|
$state = array(
|
|
'editor' => get_class($this),
|
|
'method' => (string)$method,
|
|
'token' => $token,
|
|
'hash' => $hash,
|
|
'expires' => $expires,
|
|
'meta' => is_array($meta) ? $meta : array()
|
|
);
|
|
|
|
if (!$this->writeCallbackState($token, $state)) {
|
|
return false;
|
|
}
|
|
|
|
return array(
|
|
'token' => $token,
|
|
'expires' => $expires,
|
|
'sig' => $this->createCallbackSignature($method, $token, $expires, $secret)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Verify callback request and return state.
|
|
*
|
|
* @param string $method
|
|
* @param array $post
|
|
* @param string $secret
|
|
* @param string $tokenKey
|
|
* @param string $expiresKey
|
|
* @param string $sigKey
|
|
*
|
|
* @return array|false
|
|
*/
|
|
protected function verifyCallbackRequest($method, $post, $secret, $tokenKey = 'token', $expiresKey = 'expires', $sigKey = 'sig')
|
|
{
|
|
$this->gcCallbackStates();
|
|
|
|
if (!is_array($post)) {
|
|
return false;
|
|
}
|
|
|
|
$token = isset($post[$tokenKey]) ? (string)$post[$tokenKey] : '';
|
|
$expires = isset($post[$expiresKey]) ? (string)$post[$expiresKey] : '';
|
|
$sig = isset($post[$sigKey]) ? (string)$post[$sigKey] : '';
|
|
|
|
if ($token === '' || $expires === '' || $sig === '' || !ctype_digit($expires)) {
|
|
return false;
|
|
}
|
|
|
|
$expires = (int)$expires;
|
|
if ($expires < time()) {
|
|
$this->deleteCallbackState($token);
|
|
return false;
|
|
}
|
|
|
|
$expectedSig = $this->createCallbackSignature($method, $token, $expires, $secret);
|
|
if (!$this->hashEquals($expectedSig, $sig)) {
|
|
return false;
|
|
}
|
|
|
|
$state = $this->readCallbackState($token);
|
|
if (!$state) {
|
|
return false;
|
|
}
|
|
|
|
if (empty($state['editor']) || !$this->hashEquals($state['editor'], get_class($this))
|
|
|| empty($state['method']) || !$this->hashEquals($state['method'], (string)$method)
|
|
|| empty($state['token']) || !$this->hashEquals($state['token'], $token)
|
|
|| !isset($state['expires']) || (int)$state['expires'] !== $expires
|
|
|| empty($state['hash'])) {
|
|
return false;
|
|
}
|
|
|
|
return $state;
|
|
}
|
|
|
|
/**
|
|
* Consume callback state.
|
|
*
|
|
* @param string $token
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function consumeCallbackState($token)
|
|
{
|
|
$this->deleteCallbackState($token);
|
|
}
|
|
|
|
/**
|
|
* Garbage collect expired callback states.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function gcCallbackStates()
|
|
{
|
|
$dir = $this->getCallbackStateDir(false);
|
|
if (!$dir) {
|
|
return;
|
|
}
|
|
|
|
$files = glob($dir . DIRECTORY_SEPARATOR . '*.json');
|
|
if (!$files) {
|
|
return;
|
|
}
|
|
|
|
$now = time();
|
|
foreach ($files as $path) {
|
|
if (!is_file($path)) {
|
|
continue;
|
|
}
|
|
|
|
$remove = false;
|
|
$json = file_get_contents($path);
|
|
if ($json === false || $json === '') {
|
|
$remove = true;
|
|
} else {
|
|
$state = json_decode($json, true);
|
|
if (!is_array($state) || empty($state['expires']) || (int)$state['expires'] < $now) {
|
|
$remove = true;
|
|
}
|
|
}
|
|
|
|
if ($remove) {
|
|
@unlink($path);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return callback state directory.
|
|
*
|
|
* @param bool $create
|
|
*
|
|
* @return string|false
|
|
*/
|
|
protected function getCallbackStateDir($create = false)
|
|
{
|
|
$base = elFinder::getCommonTempPath();
|
|
if (!$base) {
|
|
return false;
|
|
}
|
|
|
|
$dir = $base . DIRECTORY_SEPARATOR . $this->callbackStateDirName;
|
|
if (!is_dir($dir)) {
|
|
if (!$create || !@mkdir($dir, 0700, true)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return is_writable($dir) ? $dir : false;
|
|
}
|
|
|
|
/**
|
|
* Return callback state file path.
|
|
*
|
|
* @param string $token
|
|
* @param bool $create
|
|
*
|
|
* @return string|false
|
|
*/
|
|
protected function getCallbackStatePath($token, $create = false)
|
|
{
|
|
$dir = $this->getCallbackStateDir($create);
|
|
if (!$dir || !is_string($token) || $token === '') {
|
|
return false;
|
|
}
|
|
|
|
return $dir . DIRECTORY_SEPARATOR . hash('sha256', $token) . '.json';
|
|
}
|
|
|
|
/**
|
|
* Persist callback state.
|
|
*
|
|
* @param string $token
|
|
* @param array $state
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function writeCallbackState($token, $state)
|
|
{
|
|
$path = $this->getCallbackStatePath($token, true);
|
|
if (!$path) {
|
|
return false;
|
|
}
|
|
|
|
$json = json_encode($state);
|
|
if ($json === false) {
|
|
return false;
|
|
}
|
|
|
|
return file_put_contents($path, $json, LOCK_EX) !== false;
|
|
}
|
|
|
|
/**
|
|
* Read callback state.
|
|
*
|
|
* @param string $token
|
|
*
|
|
* @return array|false
|
|
*/
|
|
protected function readCallbackState($token)
|
|
{
|
|
$path = $this->getCallbackStatePath($token, false);
|
|
if (!$path || !is_file($path)) {
|
|
return false;
|
|
}
|
|
|
|
$json = file_get_contents($path);
|
|
if ($json === false || $json === '') {
|
|
return false;
|
|
}
|
|
|
|
$state = json_decode($json, true);
|
|
|
|
return is_array($state) ? $state : false;
|
|
}
|
|
|
|
/**
|
|
* Delete callback state.
|
|
*
|
|
* @param string $token
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function deleteCallbackState($token)
|
|
{
|
|
$path = $this->getCallbackStatePath($token, false);
|
|
if ($path && is_file($path)) {
|
|
@unlink($path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create callback signature.
|
|
*
|
|
* @param string $method
|
|
* @param string $token
|
|
* @param int $expires
|
|
* @param string $secret
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function createCallbackSignature($method, $token, $expires, $secret)
|
|
{
|
|
$payload = implode('|', array(get_class($this), (string)$method, (string)$token, (string)$expires));
|
|
|
|
return hash_hmac('sha256', $payload, (string)$secret);
|
|
}
|
|
|
|
/**
|
|
* Create random callback state token.
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function createCallbackStateToken()
|
|
{
|
|
if (function_exists('random_bytes')) {
|
|
return bin2hex(random_bytes(32));
|
|
}
|
|
|
|
if (function_exists('openssl_random_pseudo_bytes')) {
|
|
$bytes = openssl_random_pseudo_bytes(32);
|
|
if ($bytes !== false) {
|
|
return bin2hex($bytes);
|
|
}
|
|
}
|
|
|
|
return md5(uniqid(mt_rand(), true)) . md5(uniqid(mt_rand(), true));
|
|
}
|
|
|
|
/**
|
|
* Constant-time string comparison.
|
|
*
|
|
* @param string $known
|
|
* @param string $user
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function hashEquals($known, $user)
|
|
{
|
|
if (function_exists('hash_equals')) {
|
|
return hash_equals((string)$known, (string)$user);
|
|
}
|
|
|
|
$known = (string)$known;
|
|
$user = (string)$user;
|
|
if (strlen($known) !== strlen($user)) {
|
|
return false;
|
|
}
|
|
|
|
$result = 0;
|
|
$length = strlen($known);
|
|
for ($i = 0; $i < $length; $i++) {
|
|
$result |= ord($known[$i]) ^ ord($user[$i]);
|
|
}
|
|
|
|
return $result === 0;
|
|
}
|
|
}
|