dfm-8.x-1.16/src/Dfm.php
src/Dfm.php
<?php
namespace Drupal\dfm;
use Drupal\Core\DrupalKernel;
use Drupal\Core\File\FileSystem;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Drupal\file\FileInterface;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Dfm container class for helper methods.
*/
class Dfm implements TrustedCallbackInterface {
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['preRenderTextarea'];
}
/**
* Checks if a user has a dfm profile assigned for a file scheme.
*/
public static function access(AccountProxyInterface $user = NULL, $scheme = NULL) {
return (bool) static::userProfile($user, $scheme);
}
/**
* Returns a response for a dfm request.
*/
public static function response(Request $request, AccountProxyInterface $user = NULL, $scheme = NULL) {
// Ajax request.
if ($request->request->has('ajaxOp')) {
$fm = static::userFm($user, $scheme, $request);
if ($fm) {
return $fm->run();
}
}
// Return dfm page.
$page = ['#theme' => 'dfm_page'];
return new Response(\Drupal::service('renderer')->render($page));
}
/**
* Returns a file manager instance for a user.
*/
public static function userFm(AccountProxyInterface $user = NULL, $scheme = NULL, Request $request = NULL) {
$conf = static::userConf($user, $scheme);
if ($conf) {
if (!isset($conf['ajaxOp']) && $request) {
$op = $request->request->get('ajaxOp');
if ($op) {
$conf['ajaxOp'] = $op;
}
}
require_once $conf['scriptDirPath'] . '/core/Dfm.php';
return new \Dfm($conf);
}
}
/**
* Returns dfm configuration profile for a user.
*/
public static function userProfile(AccountProxyInterface $user = NULL, $scheme = NULL) {
$profiles = &drupal_static(__METHOD__, []);
$user = $user ?: \Drupal::currentUser();
$scheme = $scheme ?? \Drupal::config('system.file')->get('default_scheme');
$profile = &$profiles[$user->id()][$scheme];
if (!isset($profile)) {
// Check stream wrapper.
if (\Drupal::service('stream_wrapper_manager')->getViaScheme($scheme)) {
// Give user #1 admin profile.
$storage = \Drupal::entityTypeManager()->getStorage('dfm_profile');
if ($user->id() == 1) {
$profile = $storage->load('admin');
if ($profile) {
return $profile;
}
}
$roles_profiles = \Drupal::config('dfm.settings')->get('roles_profiles', []);
$user_roles = array_flip($user->getRoles());
// Order roles from more permissive to less permissive.
$roles = array_reverse(Role::loadMultiple());
foreach ($roles as $rid => $role) {
if (isset($user_roles[$rid]) && !empty($roles_profiles[$rid][$scheme])) {
$profile = $storage->load($roles_profiles[$rid][$scheme]);
if ($profile) {
return $profile;
}
}
}
}
$profile = FALSE;
}
return $profile;
}
/**
* Returns processed profile configuration for a user.
*/
public static function userConf(AccountProxyInterface $user = NULL, $scheme = NULL) {
$user = $user ?: \Drupal::currentUser();
$scheme = $scheme ?? \Drupal::config('system.file')->get('default_scheme');
$profile = static::userProfile($user, $scheme);
if ($profile) {
$conf = $profile->getConf();
$conf['pid'] = $profile->id();
$conf['scheme'] = $scheme;
return static::processUserConf($conf, $user);
}
}
/**
* Processes raw profile configuration of a user.
*/
public static function processUserConf(array $conf, AccountProxyInterface $user) {
// Convert MB to bytes.
$conf['uploadMaxSize'] = (int) ((float) $conf['uploadMaxSize'] * 1048576);
$conf['uploadQuota'] = (int) ((float) $conf['uploadQuota'] * 1048576);
// Set extensions.
$conf['uploadExtensions'] = array_values(array_filter(explode(' ', $conf['uploadExtensions'])));
if (isset($conf['imgExtensions'])) {
$conf['imgExtensions'] = $conf['imgExtensions']
? array_values(array_filter(explode(' ', $conf['imgExtensions'])))
: FALSE;
}
// Set variables.
$val = \Drupal::config('system.file')->get('allow_insecure_uploads');
if ($val) {
$conf['uploadInsecure'] = $val;
}
$val = \Drupal::config('dfm.settings')->get('abs_urls');
if ($val) {
$conf['absUrls'] = $val;
}
// Set paths/urls.
$url_gen = \Drupal::service('file_url_generator');
$conf['rootDirPath'] = $conf['scheme'] . '://';
$url = $url_gen->generateAbsoluteString($conf['rootDirPath'] . 'dfm111');
$conf['rootDirUrl'] = preg_replace('@/dfm111.*$@i', '', $url);
if (empty($conf['absUrls'])) {
$conf['rootDirUrl'] = $url_gen->transformRelative($conf['rootDirUrl']);
}
$conf['scriptDirPath'] = static::scriptPath();
$conf['baseUrl'] = base_path();
$conf['securityKey'] = $user->isAnonymous() ? 'anonymous' : \Drupal::csrfToken()->get('dfm');
$conf['jsCssSuffix'] = \Drupal::state()->get('system.css_js_query_string', '0');
$conf['drupalUid'] = $user->id();
$conf['fileMode'] = Settings::get('file_chmod_file', FileSystem::CHMOD_FILE);
$conf['directoryMode'] = Settings::get('file_chmod_directory', FileSystem::CHMOD_DIRECTORY);
$conf['imgJpegQuality'] = \Drupal::config('system.image.gd')->get('jpeg_quality', 85);
$conf['lang'] = \Drupal::service('language_manager')->getCurrentLanguage()->getId();
// Set thumbnail URL.
if (!empty($conf['thumbStyle']) && function_exists('image_style_options')) {
$conf['thumbUrl'] = Url::fromRoute('image.style_public', [
'image_style' => $conf['thumbStyle'],
'scheme' => $conf['scheme'],
])->toString();
if (!\Drupal::config('image.settings')->get('allow_insecure_derivatives')) {
$conf['thumbUrlQuery'] = 'dfm_itok=' . static::itok($conf['thumbStyle']);
}
}
// Add drupal plugin to call dfm_drupal_plugin_register().
$conf['plugins'][] = 'drupal';
// Set custom realpath function.
$conf['realpathFunc'] = 'Drupal\dfm\Dfm::realpath';
// Set custom url function.
if (!empty($conf['urlAlter'])) {
$conf['urlFunc'] = 'Drupal\dfm\Dfm::fileUrl';
}
// Process folder configurations. Make raw data available to alterers.
$conf['dirConfRaw'] = $conf['dirConf'];
$conf['dirConf'] = static::processConfFolders($conf, $user, \Drupal::config('dfm.settings')->get('merge_folders'));
// Run alterers.
\Drupal::moduleHandler()->alter('dfm_conf', $conf, $user);
unset($conf['dirConfRaw']);
// Apply chroot jail.
if (!empty($conf['chrootJail'])) {
static::chrootJail($conf);
}
return $conf;
}
/**
* Processes folders in a user configuration.
*/
public static function processConfFolders(array $conf, AccountProxyInterface $user, $merge = FALSE) {
$ret = static::processUserFolders($conf['dirConf'], $user);
if (!$merge) {
return $ret;
}
$scheme = $conf['scheme'];
$user_roles = array_flip($user->getRoles());
$storage = \Drupal::entityTypeManager()->getStorage('dfm_profile');
$rid_profiles = \Drupal::config('dfm.settings')->get('roles_profiles', []);
foreach ($rid_profiles as $rid => $profiles) {
$pid = $profiles[$scheme] ?? NULL;
if (!isset($user_roles[$rid]) || !$pid || $pid == $conf['pid']) {
continue;
}
/** @var \Drupal\dfm\Entity\DfmProfile $profile */
$profile = $storage->load($pid);
if (!$profile) {
continue;
}
$conf = $profile->getConf();
$dirconfs = static::processUserFolders($conf['dirConf'], $user);
$new = [];
// Inherit permissions from base to new.
foreach ($dirconfs as $dirname => $dirconf) {
$new[$dirname] = static::mergeFolderConfs($dirconf, static::inheritedFolderConf($dirname, $ret));
}
// Inherit permissions from the new to base.
// We perform bidirectional inheritance because users are usually
// confused about the order of role-profile assignments.
foreach (array_diff_key($ret, $dirconfs) as $dirname => $dirconf) {
$ret[$dirname] = static::mergeFolderConfs($dirconf, static::inheritedFolderConf($dirname, $dirconfs));
}
$ret = array_merge($ret, $new);
}
return $ret;
}
/**
* Merge multiple folder configurations into one.
*/
public static function mergeFolderConfs(array $dirconf, array ...$dirconfs) {
$subs = empty($dirconf['subdirConf']['inherit']) ? [] : NULL;
foreach ($dirconfs as $dirconf2) {
if (empty($dirconf['perms']['all'])) {
if (!empty($dirconf2['perms']['all'])) {
$dirconf['perms']['all'] = TRUE;
}
else {
foreach (($dirconf2['perms'] ?? []) as $perm => $value) {
if ($value) {
$dirconf['perms'][$perm] = TRUE;
}
}
}
}
if (isset($subs) && !empty($dirconf2['subdirConf'])) {
$sub = $dirconf2['subdirConf'];
if (!empty($sub['inherit'])) {
$sub['perms'] = $dirconf2['perms'] ?? [];
}
if (!empty($sub['perms'])) {
$subs[] = $sub;
}
}
}
if ($subs) {
$dirconf['subdirConf'] = static::mergeFolderConfs($dirconf['subdirConf'] ?? [], ...$subs);
}
return $dirconf;
}
/**
* Returns the closest inheritable conf for a folder from a set of confs.
*/
public static function inheritedFolderConf($dirname, array $dirconfs) {
// Inherit from self.
if (isset($dirconfs[$dirname])) {
return $dirconfs[$dirname];
}
// Root.
if ($dirname === '.') {
return [];
}
$inherited = [];
$inherited_depth = -2;
foreach ($dirconfs as $parent => $parent_conf) {
$subdirconf = $parent_conf['subdirConf'] ?? [];
if (!$subdirconf) {
continue;
}
$root = $parent === '.';
if (!$root && strpos($dirname . '/', $parent . '/') !== 0) {
continue;
}
$found = NULL;
$depth = $root ? -1 : substr_count($parent, '/');
// Parent is checked to apply permissions to subfolders. Use parent conf.
if (!empty($subdirconf['inherit'])) {
$found = $parent_conf;
}
// Direct parent or a grand parent with inheritance. Use sub folder conf.
elseif (!empty($subdirconf['subdirConf']['inherit']) || substr_count($dirname, '/') - $depth < 2) {
$found = $subdirconf;
$depth += 0.5;
}
// Inherit from the deepest.
if ($found && $depth > $inherited_depth) {
$inherited = $found;
$inherited_depth = $depth;
}
}
return $inherited;
}
/**
* Processes user folders.
*/
public static function processUserFolders(array $folders, AccountProxyInterface $user) {
$ret = [];
$token_service = \Drupal::token();
$token_data = ['user' => User::load($user->id())];
foreach ($folders as $folder) {
$dirname = $folder['dirname'];
// Replace tokens.
if (strpos($dirname, '[') !== FALSE) {
$dirname = $token_service->replace($dirname, $token_data);
// Unable to resolve a token.
if (strpos($dirname, ':') !== FALSE) {
continue;
}
}
if (static::regularPath($dirname)) {
$ret[$dirname] = $folder;
unset($ret[$dirname]['dirname']);
}
}
return $ret;
}
/**
* Applies chroot jail to the topmost directory in a profile configuration.
*/
public static function chrootJail(array &$conf) {
if (isset($conf['dirConf']['.'])) {
return;
}
// Set the first one as topdir.
$dirnames = array_keys($conf['dirConf']);
$topdir = array_shift($dirnames);
// Check the rest.
foreach ($dirnames as $dirname) {
// This is a subdirectory of the topdir. No change.
if (strpos($dirname . '/', $topdir . '/') === 0) {
continue;
}
// This is a parent directory of the topdir. Make it the topdir.
if (strpos($topdir . '/', $dirname . '/') === 0) {
$topdir = $dirname;
continue;
}
// Not a part of the same branch with topdir
// which means there is no top-most directory.
return;
}
// Create the new dir conf starting from the top.
$newdirconf = [];
$newdirconf['.'] = $conf['dirConf'][$topdir];
unset($conf['dirConf'][$topdir]);
// Add the rest.
$pos = strlen($topdir) + 1;
foreach ($conf['dirConf'] as $dirname => $set) {
$newdirconf[substr($dirname, $pos)] = $set;
}
$conf['dirConf'] = $newdirconf;
$conf['rootDirPath'] .= (substr($conf['rootDirPath'], -1) == '/' ? '' : '/') . $topdir;
$topdirurl = str_replace('%2F', '/', rawurlencode($topdir));
$conf['rootDirUrl'] .= '/' . $topdirurl;
// Also alter thumnail prefix.
if (isset($conf['thumbUrl'])) {
$conf['thumbUrl'] .= '/' . $topdirurl;
}
return $topdir;
}
/**
* Checks the structure of a folder path.
*
* Forbids current/parent directory notations.
*/
public static function regularPath($path) {
return is_string($path) && ($path === '.' || !preg_match('@\\\\|(^|/)\.*(/|$)@', $path));
}
/**
* Returns a managed file entity by uri.
*
* Optionally creates it.
*
* @return \Drupal\file\FileInterface
* Drupal File entity.
*/
public static function getFileEntity($uri, $create = FALSE, $save = FALSE) {
$file = FALSE;
$files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $uri]);
if ($files) {
$file = reset($files);
}
elseif ($create) {
$file = static::createFileEntity($uri, $save);
}
return $file;
}
/**
* Creates a file entity with an uri.
*
* @return \Drupal\file\FileInterface
* Drupal File entity.
*/
public static function createFileEntity($uri, $save = FALSE) {
// Set defaults. Mime and name are set by File::preCreate()
$values = is_array($uri) ? $uri : ['uri' => $uri];
$values += ['uid' => \Drupal::currentUser()->id(), 'status' => 1];
if (!isset($values['filesize'])) {
$values['filesize'] = filesize($values['uri']);
}
$file = \Drupal::entityTypeManager()->getStorage('file')->create($values);
if ($save) {
$file->save();
}
return $file;
}
/**
* Returns all references to a file except own references.
*/
public static function getFileUsage($file, $include_own = FALSE) {
$usage = \Drupal::service('file.usage')->listUsage($file);
// Remove own usage and also imce usage.
if (!$include_own) {
unset($usage['dfm'], $usage['imce']);
}
return $usage;
}
/**
* Checks if the selected relative paths are accessible by a user with Dfm.
*
* Returns the accessible file uris.
*/
public static function checkFilePaths(array $paths, AccountProxyInterface $user = NULL, $scheme = NULL) {
$ret = [];
$fm = static::userFm($user, $scheme);
if ($fm) {
foreach ($paths as $path) {
$uri = $fm->checkFile($path);
if ($uri) {
$ret[$path] = $uri;
}
}
}
return $ret;
}
/**
* Checks if a file uri is accessible by a user with Dfm.
*/
public static function checkFileUri($uri, AccountProxyInterface $user = NULL) {
[$scheme, $path] = explode('://', $uri, 2);
if ($scheme && $path) {
$fm = static::userFm($user, $scheme);
if ($fm) {
return $fm->checkFileUri($uri);
}
}
}
/**
* Returns Dfm script path.
*/
public static function scriptPath($subpath = NULL) {
static $libpath;
if (!isset($libpath)) {
$libpath = \Drupal::service('extension.list.module')->getPath('dfm') . '/library';
if (!is_dir($libpath)) {
$request = \Drupal::request();
$dirs = ['', DrupalKernel::findSitePath($request), 'sites/all'];
foreach ($dirs as $dir) {
$libpath = ($dir ? "$dir/" : '') . 'libraries/dfm_lite';
if (is_dir($libpath)) {
break;
}
}
}
}
return isset($subpath) ? $libpath . '/' . $subpath : $libpath;
}
/**
* Preprocessor for Dfm page.
*/
public static function preprocessDfmPage(&$vars) {
$vars += ['title' => t('File Browser'), 'head' => ''];
$vars['lang'] = \Drupal::languageManager()->getCurrentLanguage()->getId();
$vars['libUrl'] = base_path() . static::scriptPath();
$vars['qs'] = '?' . \Drupal::state()->get('system.css_js_query_string', '0');
$vars['cssUrls']['core'] = $vars['libUrl'] . '/core/misc/dfm.css' . $vars['qs'];
$vars['jsUrls']['jquery'] = $vars['libUrl'] . '/core/misc/jquery.js';
$vars['jsUrls']['core'] = $vars['libUrl'] . '/core/misc/dfm.js' . $vars['qs'];
$vars['scriptConf']['url'] = Url::fromRoute('<current>')->toString();
}
/**
* Resolves a file uri.
*/
public static function realpath($uri) {
return \Drupal::service('file_system')->realpath($uri);
}
/**
* Custom URL handler that is called when urlAlter option is enabled.
*/
public static function fileUrl($uri) {
return \Drupal::service('file_url_generator')->generateAbsoluteString($uri);
}
/**
* Returns image token for thumbnail styles.
*/
public static function itok($style) {
$uid = \Drupal::currentUser()->id();
$key = \Drupal::service('private_key')->get();
return substr(md5(md5("$style:$uid:$key")), 8, 8);
}
/**
* Pre renders a textarea element for Dfm integration.
*/
public static function preRenderTextarea($element) {
$static = &drupal_static(__FUNCTION__, []);
$regexp = &$static['regexp'];
if (!isset($regexp)) {
$regexp = str_replace(' ', '', \Drupal::config('dfm.settings')->get('textareas', ''));
if ($regexp) {
$regexp = '@^(' . str_replace(',', '|', implode('.*', array_map('preg_quote', explode('*', $regexp)))) . ')$@';
}
}
if ($regexp && preg_match($regexp, $element['#id'])) {
if (!isset($static['access'])) {
$static['access'] = static::access();
}
if ($static['access']) {
$element['#attached']['library'][] = 'dfm/drupal.dfm.textarea';
$element['#attributes']['class'][] = 'dfm-textarea';
}
}
return $element;
}
/**
* Runs file validators and returns errors.
*/
public static function runValidators(FileInterface $file, $validators = []) {
if (!\Drupal::hasService('file.validator')) {
$func = 'file_validate';
return $func($file, $validators);
}
$errors = [];
foreach (\Drupal::service('file.validator')->validate($file, $validators) as $violation) {
$errors[] = $violation->getMessage();
}
return $errors;
}
/**
* Formats file size.
*/
public static function formatSize($size) {
$func = 'Drupal\Core\StringTranslation\ByteSizeMarkup::create';
if (!is_callable($func)) {
$func = 'format_size';
}
return $func($size);
}
}
