diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..dfe0770
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f29410a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.idea
+.vscode
+config.core.php
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f2f0121
--- /dev/null
+++ b/README.md
@@ -0,0 +1,10 @@
+## FileMan
+
+EN:
+-------------------------------------------------------------------------------
+FileMan is an add-on for MODX Revolution that allows you to attach files to MODX documents, including additional features such as grouping, extended descriptions, download count, etc.
+
+RU:
+-------------------------------------------------------------------------------
+FileMan это дополнение для MODX Revolution, которое позволяет прикреплять файлы к документам MODX, включая дополнительные возможности, такие как группировка, расширенное описания, учет количества скачиваний и пр.
+
diff --git a/_build/build.php b/_build/build.php
new file mode 100644
index 0000000..08e6010
--- /dev/null
+++ b/_build/build.php
@@ -0,0 +1,760 @@
+modx = $modx;
+ $this->modx->initialize('mgr');
+
+ $root = dirname(__FILE__, 2) . '/';
+ $core = $root . 'core/components/' . $config['name_lower'] . '/';
+ $assets = $root . 'assets/components/' . $config['name_lower'] . '/';
+
+ $this->config = array_merge([
+ 'log_level' => modX::LOG_LEVEL_INFO,
+ 'log_target' => XPDO_CLI_MODE ? 'ECHO' : 'HTML',
+
+ 'root' => $root,
+ 'build' => $root . '_build/',
+ 'elements' => $root . '_build/elements/',
+ 'resolvers' => $root . '_build/resolvers/',
+ 'core' => $core,
+ 'assets' => $assets,
+ ], $config);
+ $this->modx->setLogLevel($this->config['log_level']);
+ $this->modx->setLogTarget($this->config['log_target']);
+
+ $this->initialize();
+ }
+
+ /**
+ * @return modPackageBuilder
+ */
+ public function process()
+ {
+ $this->buildModel();
+ $this->assets();
+
+ // Add elements
+ $elements = scandir($this->config['elements']);
+ foreach ($elements as $element) {
+ if (in_array($element[0], ['_', '.'])) {
+ continue;
+ }
+ $name = preg_replace('#\.php$#', '', $element);
+ if (method_exists($this, $name)) {
+ $this->{$name}();
+ }
+ }
+
+ // Create main vehicle
+ $vehicle = $this->builder->createVehicle($this->category, $this->category_attributes);
+
+ // Files resolvers
+ $vehicle->resolve('file', [
+ 'source' => $this->config['core'],
+ 'target' => "return MODX_CORE_PATH . 'components/';",
+ ]);
+ $vehicle->resolve('file', [
+ 'source' => $this->config['assets'],
+ 'target' => "return MODX_ASSETS_PATH . 'components/';",
+ ]);
+
+ // Add resolvers into vehicle
+ $resolvers = scandir($this->config['resolvers']);
+ foreach ($resolvers as $resolver) {
+ if (in_array($resolver[0], ['_', '.'])) {
+ continue;
+ }
+ if ($vehicle->resolve('php', ['source' => $this->config['resolvers'] . $resolver])) {
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Added resolver ' . preg_replace('#\.php$#', '', $resolver));
+ }
+ }
+
+ $this->builder->putVehicle($vehicle);
+
+ $this->builder->setPackageAttributes([
+ 'changelog' => file_get_contents($this->config['core'] . 'docs/changelog.txt'),
+ 'license' => file_get_contents($this->config['core'] . 'docs/license.txt'),
+ 'readme' => file_get_contents($this->config['core'] . 'docs/readme.txt'),
+ 'requires' => [
+ 'php' => '>=7.2.0',
+ 'modx' => '>=3.0.0',
+ ],
+ ]);
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Added package attributes and setup options.');
+
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Packing up transport package zip...');
+ $this->builder->pack();
+
+ if (!empty($this->config['install'])) {
+ $this->install();
+ }
+
+ return $this->builder;
+ }
+
+
+ /**
+ * Initialize package builder
+ */
+ private function initialize()
+ {
+ $this->builder = new modPackageBuilder($this->modx);
+ $this->builder->createPackage($this->config['name_lower'], $this->config['version'], $this->config['release']);
+ $this->builder->registerNamespace($this->config['name_lower'], false, true, '{core_path}components/' . $this->config['name_lower'] . '/');
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Created Transport Package and Namespace.');
+
+ $this->category = $this->modx->newObject(modCategory::class);
+ $this->category->set('category', $this->config['name']);
+ $this->category_attributes = [
+ xPDOTransport::UNIQUE_KEY => 'category',
+ xPDOTransport::PRESERVE_KEYS => false,
+ xPDOTransport::UPDATE_OBJECT => true,
+ xPDOTransport::RELATED_OBJECTS => true,
+ xPDOTransport::RELATED_OBJECT_ATTRIBUTES => [],
+ ];
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Created main Category.');
+ }
+
+
+ /**
+ * Update the model
+ */
+ private function buildModel()
+ {
+ $schemaFile = $this->config['core'] . 'schema/' . $this->config['name_lower'] . '.mysql.schema.xml';
+ $outputDir = $this->config['core'] . 'src/';
+ if (!file_exists($schemaFile) || empty(file_get_contents($schemaFile))) {
+ return;
+ }
+
+ $manager = $this->modx->getManager();
+ $generator = $manager->getGenerator();
+ $generator->parseSchema(
+ $schemaFile,
+ $outputDir,
+ [
+ "compile" => 0,
+ "update" => 1,
+ "regenerate" => 1,
+ "namespacePrefix" => "FileMan\\"
+ ]
+ );
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Model updated');
+ }
+
+
+ /**
+ * Install nodejs and update assets
+ */
+ protected function assets()
+ {
+ $output = [];
+ if (!file_exists($this->config['build'] . 'node_modules')) {
+ putenv('PATH=' . trim(shell_exec('echo $PATH')) . ':' . dirname(MODX_BASE_PATH) . '/');
+ if (file_exists($this->config['build'] . 'package.json')) {
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Trying to install or update nodejs dependencies');
+ $output = [
+ shell_exec('cd ' . $this->config['build'] . ' && npm config set scripts-prepend-node-path true && npm install'),
+ ];
+ }
+ if (file_exists($this->config['build'] . 'gulpfile.js')) {
+ $output = array_merge($output, [
+ shell_exec('cd ' . $this->config['build'] . ' && npm link gulp'),
+ shell_exec('cd ' . $this->config['build'] . ' && gulp copy'),
+ ]);
+ }
+ if ($output) {
+ $this->modx->log(xPDO::LOG_LEVEL_INFO, implode("\n", array_map('trim', $output)));
+ }
+ }
+ if (file_exists($this->config['build'] . 'gulpfile.js')) {
+ $output = shell_exec('cd ' . $this->config['build'] . ' && gulp default 2>&1');
+ $this->modx->log(xPDO::LOG_LEVEL_INFO, 'Compile scripts and styles ' . trim($output));
+ }
+ }
+
+ /**
+ * Install package
+ */
+ private function install()
+ {
+ $signature = $this->builder->getSignature();
+ $sig = explode('-', $signature);
+ $versionSignature = explode('.', $sig[1]);
+
+ /** @var modTransportPackage $package */
+ $package = $this->modx->getObject(modTransportPackage::class, ['signature' => $signature]);
+ if (!$package) {
+ $package = $this->modx->newObject(modTransportPackage::class);
+ $package->set('signature', $signature);
+ $package->fromArray([
+ 'created' => date('Y-m-d h:i:s'),
+ 'updated' => null,
+ 'state' => 1,
+ 'workspace' => 1,
+ 'provider' => 0,
+ 'source' => $signature . '.transport.zip',
+ 'package_name' => $this->config['name'],
+ 'version_major' => $versionSignature[0],
+ 'version_minor' => !empty($versionSignature[1]) ? $versionSignature[1] : 0,
+ 'version_patch' => !empty($versionSignature[2]) ? $versionSignature[2] : 0,
+ ]);
+ if (!empty($sig[2])) {
+ $r = preg_split('#([0-9]+)#', $sig[2], -1, PREG_SPLIT_DELIM_CAPTURE);
+ if (is_array($r) && !empty($r)) {
+ $package->set('release', $r[0]);
+ $package->set('release_index', (isset($r[1]) ? $r[1] : '0'));
+ } else {
+ $package->set('release', $sig[2]);
+ }
+ }
+ $package->save();
+ }
+ $package->xpdo->packages['MODX\Revolution\\'] = $package->xpdo->packages['Revolution'];
+ if ($package->install()) {
+ $this->modx->runProcessor('System/ClearCache');
+ }
+ }
+
+ /**
+ * Add settings
+ */
+ private function settings()
+ {
+ /** @noinspection PhpIncludeInspection */
+ $settings = include($this->config['elements'] . 'settings.php');
+ if (!is_array($settings)) {
+ $this->modx->log(modX::LOG_LEVEL_ERROR, 'Could not package in System Settings');
+ return;
+ }
+ $attributes = [
+ xPDOTransport::UNIQUE_KEY => 'key',
+ xPDOTransport::PRESERVE_KEYS => true,
+ xPDOTransport::UPDATE_OBJECT => !empty($this->config['update']['settings']),
+ xPDOTransport::RELATED_OBJECTS => false,
+ ];
+ foreach ($settings as $name => $data) {
+ /** @var modSystemSetting $setting */
+ $setting = $this->modx->newObject(modSystemSetting::class);
+ $setting->fromArray(array_merge([
+ 'key' => $this->config['name_lower'] . '_' . $name,
+ 'namespace' => $this->config['name_lower'],
+ ], $data), '', true, true);
+ $vehicle = $this->builder->createVehicle($setting, $attributes);
+ $this->builder->putVehicle($vehicle);
+ }
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Packaged in ' . count($settings) . ' System Settings');
+ }
+
+
+ /**
+ * Add menus
+ */
+ private function menus()
+ {
+ /** @noinspection PhpIncludeInspection */
+ $menus = include($this->config['elements'] . 'menus.php');
+ if (!is_array($menus)) {
+ $this->modx->log(modX::LOG_LEVEL_ERROR, 'Could not package in Menus');
+
+ return;
+ }
+ $attributes = [
+ xPDOTransport::PRESERVE_KEYS => true,
+ xPDOTransport::UPDATE_OBJECT => !empty($this->config['update']['menus']),
+ xPDOTransport::UNIQUE_KEY => 'text',
+ xPDOTransport::RELATED_OBJECTS => true,
+ ];
+ if (is_array($menus)) {
+ foreach ($menus as $name => $data) {
+ /** @var modMenu $menu */
+ $menu = $this->modx->newObject(modMenu::class);
+ $menu->fromArray(array_merge([
+ 'text' => $name,
+ 'parent' => 'components',
+ 'namespace' => $this->config['name_lower'],
+ 'icon' => '',
+ 'menuindex' => 0,
+ 'params' => '',
+ 'handler' => '',
+ ], $data), '', true, true);
+ $vehicle = $this->builder->createVehicle($menu, $attributes);
+ $this->builder->putVehicle($vehicle);
+ }
+ }
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Packaged in ' . count($menus) . ' Menus');
+ }
+
+
+ /**
+ * Add Dashboard Widgets
+ */
+ private function widgets()
+ {
+ /** @noinspection PhpIncludeInspection */
+ $widgets = include($this->config['elements'] . 'widgets.php');
+ if (!is_array($widgets)) {
+ $this->modx->log(modX::LOG_LEVEL_ERROR, 'Could not package in Dashboard Widgets');
+
+ return;
+ }
+ $attributes = [
+ xPDOTransport::PRESERVE_KEYS => true,
+ xPDOTransport::UPDATE_OBJECT => !empty($this->config['update']['widgets']),
+ xPDOTransport::UNIQUE_KEY => 'name',
+ ];
+ foreach ($widgets as $name => $data) {
+ /** @var modDashboardWidget $widget */
+ $widget = $this->modx->newObject(modDashboardWidget::class);
+ $widget->fromArray(array_merge([
+ 'name' => $name,
+ 'namespace' => 'core',
+ 'lexicon' => 'core:dashboards',
+ ], $data), '', true, true);
+ $vehicle = $this->builder->createVehicle($widget, $attributes);
+ $this->builder->putVehicle($vehicle);
+ }
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Packaged in ' . count($widgets) . ' Dashboard Widgets');
+ }
+
+
+ /**
+ * Add resources
+ */
+ private function resources()
+ {
+ /** @noinspection PhpIncludeInspection */
+ $resources = include($this->config['elements'] . 'resources.php');
+ if (!is_array($resources)) {
+ $this->modx->log(modX::LOG_LEVEL_ERROR, 'Could not package in Resources');
+
+ return;
+ }
+ $attributes = [
+ xPDOTransport::UNIQUE_KEY => 'id',
+ xPDOTransport::PRESERVE_KEYS => true,
+ xPDOTransport::UPDATE_OBJECT => !empty($this->config['update']['resources']),
+ xPDOTransport::RELATED_OBJECTS => false,
+ ];
+ $objects = [];
+ foreach ($resources as $context => $items) {
+ $menuindex = 0;
+ foreach ($items as $alias => $item) {
+ if (!isset($item['id'])) {
+ $item['id'] = $this->_idx++;
+ }
+ $item['alias'] = $alias;
+ $item['context_key'] = $context;
+ $item['menuindex'] = $menuindex++;
+ $objects = array_merge(
+ $objects,
+ $this->createResource($item, $alias)
+ );
+ }
+ }
+
+ /** @var modResource $resource */
+ foreach ($objects as $resource) {
+ $vehicle = $this->builder->createVehicle($resource, $attributes);
+ $this->builder->putVehicle($vehicle);
+ }
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Packaged in ' . count($objects) . ' Resources');
+ }
+
+
+ /**
+ * Add plugins
+ */
+ private function plugins()
+ {
+ /** @noinspection PhpIncludeInspection */
+ $plugins = include($this->config['elements'] . 'plugins.php');
+ if (!is_array($plugins)) {
+ $this->modx->log(modX::LOG_LEVEL_ERROR, 'Could not package in Plugins');
+
+ return;
+ }
+ $this->category_attributes[xPDOTransport::RELATED_OBJECT_ATTRIBUTES]['Plugins'] = [
+ xPDOTransport::UNIQUE_KEY => 'name',
+ xPDOTransport::PRESERVE_KEYS => false,
+ xPDOTransport::UPDATE_OBJECT => !empty($this->config['update']['plugins']),
+ xPDOTransport::RELATED_OBJECTS => true,
+ xPDOTransport::RELATED_OBJECT_ATTRIBUTES => [
+ 'PluginEvents' => [
+ xPDOTransport::PRESERVE_KEYS => true,
+ xPDOTransport::UPDATE_OBJECT => true,
+ xPDOTransport::UNIQUE_KEY => ['pluginid', 'event'],
+ ],
+ ],
+ ];
+ $objects = [];
+ foreach ($plugins as $name => $data) {
+ /** @var modPlugin $plugin */
+ $plugin = $this->modx->newObject(modPlugin::class);
+ $plugin->fromArray(array_merge([
+ 'name' => $name,
+ 'category' => 0,
+ 'description' => @$data['description'],
+ 'plugincode' => $this::getFileContent($this->config['core'] . 'elements/plugins/' . $data['file'] . '.php'),
+ 'static' => !empty($this->config['static']['plugins']),
+ 'source' => 1,
+ 'static_file' => 'core/components/' . $this->config['name_lower'] . '/elements/plugins/' . $data['file'] . '.php',
+ ], $data), '', true, true);
+
+ $events = [];
+ if (!empty($data['events'])) {
+ foreach ($data['events'] as $event_name => $event_data) {
+ /** @var modPluginEvent $event */
+ $event = $this->modx->newObject(modPluginEvent::class);
+ $event->fromArray(array_merge([
+ 'event' => $event_name,
+ 'priority' => 0,
+ 'propertyset' => 0,
+ ], $event_data), '', true, true);
+ $events[] = $event;
+ }
+ }
+ if (!empty($events)) {
+ $plugin->addMany($events);
+ }
+ $objects[] = $plugin;
+ }
+ $this->category->addMany($objects);
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Packaged in ' . count($objects) . ' Plugins');
+ }
+
+
+ /**
+ * Add snippets
+ */
+ private function snippets()
+ {
+ /** @noinspection PhpIncludeInspection */
+ $snippets = include($this->config['elements'] . 'snippets.php');
+ if (!is_array($snippets)) {
+ $this->modx->log(modX::LOG_LEVEL_ERROR, 'Could not package in Snippets');
+ return;
+ }
+ $this->category_attributes[xPDOTransport::RELATED_OBJECT_ATTRIBUTES]['Snippets'] = [
+ xPDOTransport::PRESERVE_KEYS => false,
+ xPDOTransport::UPDATE_OBJECT => !empty($this->config['update']['snippets']),
+ xPDOTransport::UNIQUE_KEY => 'name',
+ ];
+ $objects = [];
+ foreach ($snippets as $name => $data) {
+ /** @var modSnippet $snippet */
+ $objects[$name] = $this->modx->newObject(modSnippet::class);
+ $objects[$name]->fromArray(array_merge([
+ 'id' => 0,
+ 'name' => $name,
+ 'description' => @$data['description'],
+ 'snippet' => $this::getFileContent($this->config['core'] . 'elements/snippets/' . $data['file'] . '.php'),
+ 'static' => !empty($this->config['static']['snippets']),
+ 'source' => 1,
+ 'static_file' => 'core/components/' . $this->config['name_lower'] . '/elements/snippets/' . $data['file'] . '.php',
+ ], $data), '', true, true);
+ $properties = [];
+ foreach (@$data['properties'] as $k => $v) {
+ $properties[] = array_merge([
+ 'name' => $k,
+ 'desc' => $this->config['name_lower'] . '_prop_' . $k,
+ 'lexicon' => $this->config['name_lower'] . ':properties',
+ ], $v);
+ }
+ $objects[$name]->setProperties($properties);
+ }
+ $this->category->addMany($objects);
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Packaged in ' . count($objects) . ' Snippets');
+ }
+
+
+ /**
+ * Add chunks
+ */
+ private function chunks()
+ {
+ /** @noinspection PhpIncludeInspection */
+ $chunks = include($this->config['elements'] . 'chunks.php');
+ if (!is_array($chunks)) {
+ $this->modx->log(modX::LOG_LEVEL_ERROR, 'Could not package in Chunks');
+
+ return;
+ }
+ $this->category_attributes[xPDOTransport::RELATED_OBJECT_ATTRIBUTES]['Chunks'] = [
+ xPDOTransport::PRESERVE_KEYS => false,
+ xPDOTransport::UPDATE_OBJECT => !empty($this->config['update']['chunks']),
+ xPDOTransport::UNIQUE_KEY => 'name',
+ ];
+ $objects = [];
+ foreach ($chunks as $name => $data) {
+ /** @var modChunk[] $objects */
+ $objects[$name] = $this->modx->newObject(modChunk::class);
+ $objects[$name]->fromArray(array_merge([
+ 'id' => 0,
+ 'name' => $name,
+ 'description' => @$data['description'],
+ 'snippet' => $this::getFileContent($this->config['core'] . 'elements/chunks/' . $data['file'] . '.tpl'),
+ 'static' => !empty($this->config['static']['chunks']),
+ 'source' => 1,
+ 'static_file' => 'core/components/' . $this->config['name_lower'] . '/elements/chunks/' . $data['file'] . '.tpl',
+ ], $data), '', true, true);
+ $objects[$name]->setProperties(@$data['properties']);
+ }
+ $this->category->addMany($objects);
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Packaged in ' . count($objects) . ' Chunks');
+ }
+
+
+ /**
+ * Add templates
+ */
+ private function templates()
+ {
+ /** @noinspection PhpIncludeInspection */
+ $templates = include($this->config['elements'] . 'templates.php');
+ if (!is_array($templates)) {
+ $this->modx->log(modX::LOG_LEVEL_ERROR, 'Could not package in Templates');
+
+ return;
+ }
+ $this->category_attributes[xPDOTransport::RELATED_OBJECT_ATTRIBUTES]['Templates'] = [
+ xPDOTransport::UNIQUE_KEY => 'templatename',
+ xPDOTransport::PRESERVE_KEYS => false,
+ xPDOTransport::UPDATE_OBJECT => !empty($this->config['update']['templates']),
+ xPDOTransport::RELATED_OBJECTS => false,
+ ];
+ $objects = [];
+ foreach ($templates as $name => $data) {
+ /** @var modTemplate[] $objects */
+ $objects[$name] = $this->modx->newObject(modTemplate::class);
+ $objects[$name]->fromArray(array_merge([
+ 'templatename' => $name,
+ 'description' => $data['description'],
+ 'content' => $this::getFileContent($this->config['core'] . 'elements/templates/' . $data['file'] . '.tpl'),
+ 'static' => !empty($this->config['static']['templates']),
+ 'source' => 1,
+ 'static_file' => 'core/components/' . $this->config['name_lower'] . '/elements/templates/' . $data['file'] . '.tpl',
+ ], $data), '', true, true);
+ }
+ $this->category->addMany($objects);
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Packaged in ' . count($objects) . ' Templates');
+ }
+
+
+ /**
+ * Add access policy
+ */
+ private function policies()
+ {
+ /** @noinspection PhpIncludeInspection */
+ $policies = include($this->config['elements'] . 'policies.php');
+ if (!is_array($policies)) {
+ $this->modx->log(modX::LOG_LEVEL_ERROR, 'Could not package in Access Policies');
+ return;
+ }
+ $attributes = [
+ xPDOTransport::PRESERVE_KEYS => false,
+ xPDOTransport::UNIQUE_KEY => array('name'),
+ xPDOTransport::UPDATE_OBJECT => !empty($this->config['update']['policies']),
+ ];
+ foreach ($policies as $name => $data) {
+ if (isset($data['data'])) {
+ $data['data'] = json_encode($data['data']);
+ }
+ /** @var $policy modAccessPolicy */
+ $policy = $this->modx->newObject(modAccessPolicy::class);
+ $policy->fromArray(array_merge(array(
+ 'name' => $name,
+ 'lexicon' => $this->config['name_lower'] . ':permissions',
+ ), $data)
+ , '', true, true);
+ $vehicle = $this->builder->createVehicle($policy, $attributes);
+ $this->builder->putVehicle($vehicle);
+ }
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Packaged in ' . count($policies) . ' Access Policies');
+ }
+
+
+ /**
+ * Add policy templates
+ */
+ private function policy_templates()
+ {
+ /** @noinspection PhpIncludeInspection */
+ $policy_templates = include($this->config['elements'] . 'policy_templates.php');
+ if (!is_array($policy_templates)) {
+ $this->modx->log(modX::LOG_LEVEL_ERROR, 'Could not package in Policy Templates');
+ return;
+ }
+ $attributes = [
+ xPDOTransport::PRESERVE_KEYS => false,
+ xPDOTransport::UNIQUE_KEY => array('name'),
+ xPDOTransport::UPDATE_OBJECT => !empty($this->config['update']['policy_templates']),
+ xPDOTransport::RELATED_OBJECTS => true,
+ xPDOTransport::RELATED_OBJECT_ATTRIBUTES => array(
+ 'Permissions' => array(
+ xPDOTransport::PRESERVE_KEYS => false,
+ xPDOTransport::UPDATE_OBJECT => !empty($this->config['update']['permission']),
+ xPDOTransport::UNIQUE_KEY => array('template', 'name'),
+ ),
+ ),
+ ];
+ foreach ($policy_templates as $name => $data) {
+ $permissions = array();
+ if (isset($data['permissions']) && is_array($data['permissions'])) {
+ foreach ($data['permissions'] as $name2 => $data2) {
+ /** @var $permission modAccessPermission */
+ $permission = $this->modx->newObject(modAccessPermission::class);
+ $permission->fromArray(array_merge(array(
+ 'name' => $name2,
+ 'description' => $name2,
+ 'value' => true,
+ ), $data2)
+ , '', true, true);
+ $permissions[] = $permission;
+ }
+ }
+ /** @var $permission modAccessPolicyTemplate */
+ $permission = $this->modx->newObject(modAccessPolicyTemplate::class);
+ $permission->fromArray(array_merge(array(
+ 'name' => $name,
+ 'lexicon' => $this->config['name_lower'] . ':permissions',
+ ), $data)
+ , '', true, true);
+ if (!empty($permissions)) {
+ $permission->addMany($permissions);
+ }
+ $vehicle = $this->builder->createVehicle($permission, $attributes);
+ $this->builder->putVehicle($vehicle);
+ }
+ $this->modx->log(modX::LOG_LEVEL_INFO, 'Packaged in ' . count($policy_templates) . ' Access Policy Templates');
+ }
+
+ /**
+ * @param $filename
+ *
+ * @return string
+ */
+ private function getFileContent($filename)
+ {
+ if (file_exists($filename)) {
+ $file = trim(file_get_contents($filename));
+
+ return preg_match('#\<\?php(.*)#is', $file, $data)
+ ? rtrim(rtrim(trim(@$data[1]), '?>'))
+ : $file;
+ }
+
+ return '';
+ }
+
+ /**
+ * @param array $data
+ * @param string $uri
+ * @param int $parent
+ *
+ * @return array
+ */
+ protected function createResource(array $data, $uri, $parent = 0)
+ {
+ $file = $data['context_key'] . '/' . $uri;
+ /** @var modResource $resource */
+ $resource = $this->modx->newObject(modResource::class);
+ $resource->fromArray(array_merge([
+ 'parent' => $parent,
+ 'published' => true,
+ 'deleted' => false,
+ 'hidemenu' => false,
+ 'createdon' => time(),
+ 'template' => 1,
+ 'isfolder' => !empty($data['isfolder']) || !empty($data['resources']),
+ 'uri' => $uri,
+ 'uri_override' => false,
+ 'richtext' => false,
+ 'searchable' => true,
+ 'content' => $this::getFileContent($this->config['core'] . 'elements/resources/' . $file . '.tpl'),
+ ], $data), '', true, true);
+
+ if (!empty($data['groups'])) {
+ foreach ($data['groups'] as $group) {
+ $resource->joinGroup($group);
+ }
+ }
+ $resources[] = $resource;
+
+ if (!empty($data['resources'])) {
+ $menuindex = 0;
+ foreach ($data['resources'] as $alias => $item) {
+ if (!isset($item['id'])) {
+ $item['id'] = $this->_idx++;
+ }
+ $item['alias'] = $alias;
+ $item['context_key'] = $data['context_key'];
+ $item['menuindex'] = $menuindex++;
+ $resources = array_merge(
+ $resources,
+ $this->createResource($item, $uri . '/' . $alias, $data['id'])
+ );
+ }
+ }
+
+ return $resources;
+ }
+}
+
+/** @var array $config */
+if (!file_exists(dirname(__FILE__) . '/config.inc.php')) {
+ exit('Could not load MODX config. Please specify correct MODX_CORE_PATH constant in config file!');
+}
+$config = require(dirname(__FILE__) . '/config.inc.php');
+require_once MODX_CORE_PATH . 'model/modx/modx.class.php';
+$modx = new modX();
+$install = new FileManPackage($modx, $config);
+$builder = $install->process();
+
+if (!empty($config['download'])) {
+ $name = $builder->getSignature() . '.transport.zip';
+ if ($content = file_get_contents(MODX_CORE_PATH . '/packages/' . $name)) {
+ header('Content-Description: File Transfer');
+ header('Content-Type: application/octet-stream');
+ header('Content-Disposition: attachment; filename=' . $name);
+ header('Content-Transfer-Encoding: binary');
+ header('Expires: 0');
+ header('Cache-Control: must-revalidate');
+ header('Pragma: public');
+ header('Content-Length: ' . strlen($content));
+ exit($content);
+ }
+}
diff --git a/_build/config.inc.php b/_build/config.inc.php
new file mode 100644
index 0000000..b064d41
--- /dev/null
+++ b/_build/config.inc.php
@@ -0,0 +1,43 @@
+ 1)) {
+ $path = dirname($path);
+ }
+ define('MODX_CORE_PATH', $path . '/core/');
+}
+
+return [
+ 'name' => 'FileMan',
+ 'name_lower' => 'fileman',
+ 'version' => '3.0.0',
+ 'release' => 'beta',
+ // Install package to site right after build
+ 'install' => true,
+ // Which elements should be updated on package upgrade
+ 'update' => [
+ 'chunks' => true,
+ 'menus' => true,
+ 'permission' => true,
+ 'plugins' => true,
+ 'policies' => true,
+ 'policy_templates' => true,
+ 'resources' => false,
+ 'settings' => false,
+ 'snippets' => true,
+ 'templates' => false,
+ 'widgets' => false,
+ ],
+ // Which elements should be static by default
+ 'static' => [
+ 'plugins' => false,
+ 'snippets' => false,
+ 'chunks' => false,
+ ],
+ // Log settings
+ 'log_level' => !empty($_REQUEST['download']) ? 0 : 3,
+ 'log_target' => php_sapi_name() == 'cli' ? 'ECHO' : 'HTML',
+ // Download transport.zip after build
+ 'download' => !empty($_REQUEST['download']),
+];
diff --git a/_build/elements/_policies.php b/_build/elements/_policies.php
new file mode 100644
index 0000000..8cdd1a5
--- /dev/null
+++ b/_build/elements/_policies.php
@@ -0,0 +1,10 @@
+ [
+ 'description' => 'FileMan policy description.',
+ 'data' => [
+ 'fileman_save' => true,
+ ]
+ ],
+];
diff --git a/_build/elements/_policy_templates.php b/_build/elements/_policy_templates.php
new file mode 100644
index 0000000..1377401
--- /dev/null
+++ b/_build/elements/_policy_templates.php
@@ -0,0 +1,11 @@
+ [
+ 'description' => 'FileMan policy template description.',
+ 'template_group' => 1,
+ 'permissions' => [
+ 'fileman_save' => [],
+ ]
+ ],
+];
diff --git a/_build/elements/_resources.php b/_build/elements/_resources.php
new file mode 100644
index 0000000..d84ed01
--- /dev/null
+++ b/_build/elements/_resources.php
@@ -0,0 +1,33 @@
+ [
+ 'index' => [
+ 'pagetitle' => 'Home',
+ 'template' => 1,
+ 'hidemenu' => false,
+ ],
+ 'service' => [
+ 'pagetitle' => 'Service',
+ 'template' => 0,
+ 'hidemenu' => true,
+ 'published' => false,
+ 'resources' => [
+ '404' => [
+ 'pagetitle' => '404',
+ 'template' => 1,
+ 'hidemenu' => true,
+ 'uri' => '404',
+ 'uri_override' => true,
+ ],
+ 'sitemap.xml' => [
+ 'pagetitle' => 'Sitemap',
+ 'template' => 0,
+ 'hidemenu' => true,
+ 'uri' => 'sitemap.xml',
+ 'uri_override' => true,
+ ],
+ ],
+ ],
+ ],
+];
\ No newline at end of file
diff --git a/_build/elements/_templates.php b/_build/elements/_templates.php
new file mode 100644
index 0000000..d4d3cd1
--- /dev/null
+++ b/_build/elements/_templates.php
@@ -0,0 +1,8 @@
+ [
+ 'file' => 'base',
+ 'description' => 'Base template',
+ ],
+];
diff --git a/_build/elements/_widgets.php b/_build/elements/_widgets.php
new file mode 100644
index 0000000..9fea7d6
--- /dev/null
+++ b/_build/elements/_widgets.php
@@ -0,0 +1,12 @@
+ [
+ 'description' => '',
+ 'type' => 'file',
+ 'content' => '',
+ 'namespace' => 'fileman',
+ 'lexicon' => 'fileman:dashboards',
+ 'size' => 'half',
+ ],
+];
diff --git a/_build/elements/chunks.php b/_build/elements/chunks.php
new file mode 100644
index 0000000..38d68c6
--- /dev/null
+++ b/_build/elements/chunks.php
@@ -0,0 +1,8 @@
+ array(
+ 'file' => 'files',
+ 'description' => '',
+ )
+];
diff --git a/_build/elements/menus.php b/_build/elements/menus.php
new file mode 100644
index 0000000..341dcdc
--- /dev/null
+++ b/_build/elements/menus.php
@@ -0,0 +1,9 @@
+ [
+ 'description' => 'fileman_menu_desc',
+ 'action' => 'home',
+ //'icon' => ' ',
+ ],
+];
diff --git a/_build/elements/plugins.php b/_build/elements/plugins.php
new file mode 100644
index 0000000..b1706da
--- /dev/null
+++ b/_build/elements/plugins.php
@@ -0,0 +1,12 @@
+ array(
+ 'file' => 'fileman',
+ 'description' => '',
+ 'events' => array(
+ 'OnDocFormPrerender' => array(),
+ 'OnEmptyTrash' => array()
+ )
+ )
+];
diff --git a/_build/elements/settings.php b/_build/elements/settings.php
new file mode 100644
index 0000000..73884b2
--- /dev/null
+++ b/_build/elements/settings.php
@@ -0,0 +1,36 @@
+ array(
+ 'xtype' => 'modx-combo-source',
+ 'value' => 1
+ ),
+ 'path' => array(
+ 'xtype' => 'textfield',
+ 'value' => 'files/{resource}/'
+ ),
+ 'templates' => array(
+ 'xtype' => 'textfield',
+ 'value' => ''
+ ),
+ 'calchash' => array(
+ 'xtype' => 'combo-boolean',
+ 'value' => false
+ ),
+ 'private' => array(
+ 'xtype' => 'combo-boolean',
+ 'value' => false
+ ),
+ 'download' => array(
+ 'xtype' => 'combo-boolean',
+ 'value' => true
+ ),
+ 'auto_title' => array(
+ 'xtype' => 'combo-boolean',
+ 'value' => true
+ ),
+ 'grid_fields' => array(
+ 'xtype' => 'textfield',
+ 'value' => 'id,name,title,description,group,private,download',
+ ),
+];
diff --git a/_build/elements/snippets.php b/_build/elements/snippets.php
new file mode 100644
index 0000000..f0c6eae
--- /dev/null
+++ b/_build/elements/snippets.php
@@ -0,0 +1,66 @@
+ [
+ 'file' => 'files',
+ 'description' => 'FileMan snippet to list files',
+ 'properties' => [
+ 'tpl' => array(
+ 'type' => 'textfield',
+ 'value' => 'tpl.FileMan.Files',
+ ),
+ 'sortBy' => array(
+ 'type' => 'textfield',
+ 'value' => 'sort_order',
+ ),
+ 'sortDir' => array(
+ 'type' => 'list',
+ 'options' => array(
+ array('text' => 'ASC', 'value' => 'ASC'),
+ array('text' => 'DESC', 'value' => 'DESC'),
+ ),
+ 'value' => 'ASC'
+ ),
+ 'limit' => array(
+ 'type' => 'numberfield',
+ 'value' => 0,
+ ),
+ 'offset' => array(
+ 'type' => 'numberfield',
+ 'value' => 0,
+ ),
+ 'totalVar' => array(
+ 'type' => 'textfield',
+ 'value' => 'total',
+ ),
+ 'toPlaceholder' => array(
+ 'type' => 'combo-boolean',
+ 'value' => false,
+ ),
+ 'ids' => array(
+ 'type' => 'textfield',
+ 'value' => '',
+ ),
+ 'resource' => array(
+ 'type' => 'numberfield',
+ 'value' => 0,
+ ),
+ 'showGroups' => array(
+ 'type' => 'combo-boolean',
+ 'value' => true,
+ ),
+ 'makeUrl' => array(
+ 'type' => 'combo-boolean',
+ 'value' => true,
+ ),
+ 'privateUrl' => array(
+ 'type' => 'combo-boolean',
+ 'value' => false,
+ ),
+ 'includeTimeStamp' => array(
+ 'type' => 'combo-boolean',
+ 'value' => false,
+ ),
+ ],
+ ],
+];
diff --git a/_build/resolvers/_policy.php b/_build/resolvers/_policy.php
new file mode 100644
index 0000000..4379aba
--- /dev/null
+++ b/_build/resolvers/_policy.php
@@ -0,0 +1,33 @@
+xpdo) {
+ $modx = $transport->xpdo;
+ switch ($options[xPDOTransport::PACKAGE_ACTION]) {
+ case xPDOTransport::ACTION_INSTALL:
+ case xPDOTransport::ACTION_UPGRADE:
+ // Assign policy to template
+ $policy = $modx->getObject(modAccessPolicy::class, array('name' => 'FileManUserPolicy'));
+ if ($policy) {
+ $template = $modx->getObject(modAccessPolicyTemplate::class, ['name' => 'FileManUserPolicyTemplate']);
+ if ($template) {
+ $policy->set('template', $template->get('id'));
+ $policy->save();
+ } else {
+ $modx->log(
+ xPDO::LOG_LEVEL_ERROR,
+ '[FileMan] Could not find FileManUserPolicyTemplate Access Policy Template!'
+ );
+ }
+ } else {
+ $modx->log(xPDO::LOG_LEVEL_ERROR, '[FileMan] Could not find FileManUserPolicyTemplate Access Policy!');
+ }
+ break;
+ }
+}
+return true;
diff --git a/_build/resolvers/_setup.php b/_build/resolvers/_setup.php
new file mode 100644
index 0000000..0800010
--- /dev/null
+++ b/_build/resolvers/_setup.php
@@ -0,0 +1,134 @@
+xpdo || !($transport instanceof xPDOTransport)) {
+ return false;
+}
+
+$modx = $transport->xpdo;
+$packages = [
+ 'Ace' => [
+ 'version' => '1.9.3-pl',
+ 'service_url' => 'modstore.pro',
+ ],
+ 'pdoTools' => [
+ 'version' => '3.0.0-beta',
+ 'service_url' => 'modstore.pro',
+ ],
+];
+
+$downloadPackage = function ($src, $dst) {
+ if (ini_get('allow_url_fopen')) {
+ $file = @file_get_contents($src);
+ } else {
+ if (function_exists('curl_init')) {
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $src);
+ curl_setopt($ch, CURLOPT_HEADER, 0);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 180);
+ $safeMode = @ini_get('safe_mode');
+ $openBasedir = @ini_get('open_basedir');
+ if (empty($safeMode) && empty($openBasedir)) {
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
+ }
+
+ $file = curl_exec($ch);
+ curl_close($ch);
+ } else {
+ return false;
+ }
+ }
+ file_put_contents($dst, $file);
+
+ return file_exists($dst);
+};
+
+$installPackage = function ($packageName, $options = []) use ($modx, $downloadPackage) {
+ /** @var modTransportProvider $provider */
+ if (!empty($options['service_url'])) {
+ $provider = $modx->getObject(modTransportProvider::class, [
+ 'service_url:LIKE' => '%' . $options['service_url'] . '%',
+ ]);
+ }
+ if (empty($provider)) {
+ $provider = $modx->getObject(modTransportProvider::class, 1);
+ }
+ $modx->getVersionData();
+ $productVersion = $modx->version['code_name'] . '-' . $modx->version['full_version'];
+
+ $response = $provider->request('package', 'GET', [
+ 'supports' => $productVersion,
+ 'query' => $packageName,
+ ]);
+
+ if (empty($response)) {
+ return [
+ 'success' => 0,
+ 'message' => "Could not find {$packageName} in MODX repository",
+ ];
+ }
+
+ $foundPackages = simplexml_load_string($response->getBody()->getContents());
+ foreach ($foundPackages as $foundPackage) {
+ /** @var modTransportPackage $foundPackage */
+ /** @noinspection PhpUndefinedFieldInspection */
+ if ((string)$foundPackage->name === $packageName) {
+ $sig = explode('-', (string)$foundPackage->signature);
+ $versionSignature = explode('.', $sig[1]);
+ /** @var modTransportPackage $package */
+ $package = $provider->transfer((string)$foundPackage->signature);
+ if ($package && $package->install()) {
+ return [
+ 'success' => 1,
+ 'message' => "{$packageName} was successfully installed",
+ ];
+ }
+ return [
+ 'success' => 0,
+ 'message' => "Could not save package {$packageName} ",
+ ];
+ }
+ }
+
+ return true;
+};
+
+$success = false;
+switch ($options[xPDOTransport::PACKAGE_ACTION]) {
+ case xPDOTransport::ACTION_INSTALL:
+ case xPDOTransport::ACTION_UPGRADE:
+ foreach ($packages as $name => $data) {
+ if (!is_array($data)) {
+ $data = ['version' => $data];
+ }
+ $installed = $modx->getIterator(modTransportPackage::class, ['package_name' => $name]);
+ /** @var modTransportPackage $package */
+ foreach ($installed as $package) {
+ if ($package->compareVersion($data['version'], '<=')) {
+ continue(2);
+ }
+ }
+ $modx->log(modX::LOG_LEVEL_INFO, "Trying to install {$name} . Please wait...");
+ $response = $installPackage($name, $data);
+ if (is_array($response)) {
+ $level = $response['success']
+ ? modX::LOG_LEVEL_INFO
+ : modX::LOG_LEVEL_ERROR;
+ $modx->log($level, $response['message']);
+ }
+ }
+ $success = true;
+ break;
+
+ case xPDOTransport::ACTION_UNINSTALL:
+ $success = true;
+ break;
+}
+
+return $success;
diff --git a/_build/resolvers/_symlinks.php b/_build/resolvers/_symlinks.php
new file mode 100644
index 0000000..9570c23
--- /dev/null
+++ b/_build/resolvers/_symlinks.php
@@ -0,0 +1,30 @@
+xpdo) {
+ $modx = $transport->xpdo;
+
+ $dev = MODX_BASE_PATH . 'Extras/FileMan/';
+ /** @var xPDOCacheManager $cache */
+ $cache = $modx->getCacheManager();
+ if (file_exists($dev) && $cache) {
+ if (!is_link($dev . 'assets/components/fileman')) {
+ $cache->deleteTree(
+ $dev . 'assets/components/fileman/',
+ ['deleteTop' => true, 'skipDirs' => false, 'extensions' => []]
+ );
+ symlink(MODX_ASSETS_PATH . 'components/fileman/', $dev . 'assets/components/fileman');
+ }
+ if (!is_link($dev . 'core/components/fileman')) {
+ $cache->deleteTree(
+ $dev . 'core/components/fileman/',
+ ['deleteTop' => true, 'skipDirs' => false, 'extensions' => []]
+ );
+ symlink(MODX_CORE_PATH . 'components/fileman/', $dev . 'core/components/fileman');
+ }
+ }
+}
+
+return true;
diff --git a/_build/resolvers/tables.php b/_build/resolvers/tables.php
new file mode 100644
index 0000000..41d4ef6
--- /dev/null
+++ b/_build/resolvers/tables.php
@@ -0,0 +1,110 @@
+xpdo) {
+ $modx = $transport->xpdo;
+
+ switch ($options[xPDOTransport::PACKAGE_ACTION]) {
+ case xPDOTransport::ACTION_INSTALL:
+ case xPDOTransport::ACTION_UPGRADE:
+ $modx->addPackage('FileMan\Model', MODX_CORE_PATH . 'components/fileman/src/', null, 'FileMan\\');
+ $manager = $modx->getManager();
+ $objects = [];
+ $schemaFile = MODX_CORE_PATH . 'components/fileman/schema/fileman.mysql.schema.xml';
+ if (is_file($schemaFile)) {
+ $schema = new SimpleXMLElement($schemaFile, 0, true);
+ if (isset($schema->object)) {
+ foreach ($schema->object as $obj) {
+ $objects[] = (string)$obj['class'];
+ }
+ }
+ unset($schema);
+ }
+ foreach ($objects as $class) {
+ $class = 'FileMan\\Model\\' . $class;
+ $table = $modx->getTableName($class);
+ $sql = "SHOW TABLES LIKE '" . trim($table, '`') . "'";
+ $stmt = $modx->prepare($sql);
+ $newTable = true;
+ if ($stmt->execute() && $stmt->fetchAll()) {
+ $newTable = false;
+ }
+ // If the table is just created
+ if ($newTable) {
+ $manager->createObjectContainer($class);
+ } else {
+ // If the table exists
+ // 1. Operate with tables
+ $tableFields = [];
+ $c = $modx->prepare("SHOW COLUMNS IN {$modx->getTableName($class)}");
+ $c->execute();
+ while ($cl = $c->fetch(PDO::FETCH_ASSOC)) {
+ $tableFields[$cl['Field']] = $cl['Field'];
+ }
+ foreach ($modx->getFields($class) as $field => $v) {
+ if (in_array($field, $tableFields)) {
+ unset($tableFields[$field]);
+ $manager->alterField($class, $field);
+ } else {
+ $manager->addField($class, $field);
+ }
+ }
+ foreach ($tableFields as $field) {
+ $manager->removeField($class, $field);
+ }
+ // 2. Operate with indexes
+ $indexes = [];
+ $c = $modx->prepare("SHOW INDEX FROM {$modx->getTableName($class)}");
+ $c->execute();
+ while ($row = $c->fetch(PDO::FETCH_ASSOC)) {
+ $name = $row['Key_name'];
+ if (!isset($indexes[$name])) {
+ $indexes[$name] = [$row['Column_name']];
+ } else {
+ $indexes[$name][] = $row['Column_name'];
+ }
+ }
+ foreach ($indexes as $name => $values) {
+ sort($values);
+ $indexes[$name] = implode(':', $values);
+ }
+ $map = $modx->getIndexMeta($class);
+ // Remove old indexes
+ foreach ($indexes as $key => $index) {
+ if (!isset($map[$key])) {
+ if ($manager->removeIndex($class, $key)) {
+ $modx->log(modX::LOG_LEVEL_INFO, "Removed index \"{$key}\" of the table \"{$class}\"");
+ }
+ }
+ }
+ // Add or alter existing
+ foreach ($map as $key => $index) {
+ ksort($index['columns']);
+ $index = implode(':', array_keys($index['columns']));
+ if (!isset($indexes[$key])) {
+ if ($manager->addIndex($class, $key)) {
+ $modx->log(modX::LOG_LEVEL_INFO, "Added index \"{$key}\" in the table \"{$class}\"");
+ }
+ } else {
+ if ($index != $indexes[$key]) {
+ if ($manager->removeIndex($class, $key) && $manager->addIndex($class, $key)) {
+ $modx->log(modX::LOG_LEVEL_INFO,
+ "Updated index \"{$key}\" of the table \"{$class}\""
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+ break;
+
+ case xPDOTransport::ACTION_UNINSTALL:
+ break;
+ }
+}
+
+return true;
diff --git a/assets/components/fileman/connector.php b/assets/components/fileman/connector.php
new file mode 100644
index 0000000..b8079e2
--- /dev/null
+++ b/assets/components/fileman/connector.php
@@ -0,0 +1,48 @@
+services->get('FileMan');
+$modx->lexicon->load('fileman:default');
+
+// handle request
+$corePath = $modx->getOption('fileman_core_path', null, $modx->getOption('core_path') . 'components/fileman/');
+$path = $modx->getOption(
+ 'processorsPath',
+ $fileMan->config,
+ $corePath . 'src/Processors/'
+);
+$modx->getRequest();
+
+if($action == 'download') {
+ $action = 'File\\WebDownload';
+}
+
+$requestOptions = [
+ 'action' => 'FileMan\\Processors\\' . $action,
+ 'processors_path' => $path,
+ 'location' => '',
+];
+
+$modx->request->handleRequest($requestOptions);
\ No newline at end of file
diff --git a/assets/components/fileman/css/index.html b/assets/components/fileman/css/index.html
new file mode 100644
index 0000000..e69de29
diff --git a/assets/components/fileman/css/mgr/main.css b/assets/components/fileman/css/mgr/main.css
new file mode 100644
index 0000000..e706ee4
--- /dev/null
+++ b/assets/components/fileman/css/mgr/main.css
@@ -0,0 +1,36 @@
+.fileman-window-url-description {
+ margin: 13px 0 0 0;
+ background: #eeeeee;
+ padding: 8px 8px;
+ border-radius: 3px;
+ font-style: italic;
+ color: #868686;
+}
+
+
+.fileman-preview {
+ min-width: 108px;
+ min-height: 108px;
+ padding-top: 0 !important;
+ vertical-align: middle;
+ height: 108px;
+ line-height: 165px;
+ background-color: #f2f2f2;
+ border: 1px dashed #adadad;
+
+ display: flex;
+ flex-wrap: nowrap;
+ align-content: center;
+ justify-content: center;
+ align-items: center;
+}
+
+.fileman-preview img {
+ display: block;
+ max-width: 100%;
+ max-height: 100%;
+}
+.fileman-preview i {
+ font-size: 40px;
+ color: #607d8b;
+}
\ No newline at end of file
diff --git a/assets/components/fileman/download.php b/assets/components/fileman/download.php
new file mode 100644
index 0000000..80d3ff4
--- /dev/null
+++ b/assets/components/fileman/download.php
@@ -0,0 +1,41 @@
+services->add('error', new modError($modx));
+$modx->error = $modx->services->get('error');
+$modx->getRequest();
+$modx->setLogLevel(modX::LOG_LEVEL_ERROR);
+$modx->setLogTarget('FILE');
+$modx->error->message = null;
+
+// Get properties
+$properties = array();
+
+/** @var FileMan $fileMan */
+define('MODX_ACTION_MODE', true);
+try {
+ $fileMan = $modx->services->get('FileMan');
+} catch (ContainerException | NotFoundException $e) {
+ $modx->log(modX::LOG_LEVEL_ERROR, "[FileMan] Can't get FileMan service.");
+ return false;
+}
+
+$fileMan->download($fid);
\ No newline at end of file
diff --git a/assets/components/fileman/img/preview.png b/assets/components/fileman/img/preview.png
new file mode 100644
index 0000000..6ad7a92
Binary files /dev/null and b/assets/components/fileman/img/preview.png differ
diff --git a/assets/components/fileman/index.html b/assets/components/fileman/index.html
new file mode 100644
index 0000000..e69de29
diff --git a/assets/components/fileman/js/index.html b/assets/components/fileman/js/index.html
new file mode 100644
index 0000000..e69de29
diff --git a/assets/components/fileman/js/mgr/fileman.js b/assets/components/fileman/js/mgr/fileman.js
new file mode 100644
index 0000000..7739d27
--- /dev/null
+++ b/assets/components/fileman/js/mgr/fileman.js
@@ -0,0 +1,10 @@
+let FileMan = function (config) {
+ config = config || {};
+ FileMan.superclass.constructor.call(this, config);
+};
+Ext.extend(FileMan, Ext.Component, {
+ page: {}, window: {}, grid: {}, tree: {}, panel: {}, combo: {}, config: {}, view: {}, utils: {}
+});
+Ext.reg('fileman', FileMan);
+
+FileMan = new FileMan();
\ No newline at end of file
diff --git a/assets/components/fileman/js/mgr/misc/combo.js b/assets/components/fileman/js/mgr/misc/combo.js
new file mode 100644
index 0000000..eab9d93
--- /dev/null
+++ b/assets/components/fileman/js/mgr/misc/combo.js
@@ -0,0 +1,48 @@
+FileMan.combo.Search = function (config) {
+ config = config || {};
+ Ext.applyIf(config, {
+ xtype: 'twintrigger',
+ ctCls: 'x-field-search',
+ allowBlank: true,
+ msgTarget: 'under',
+ emptyText: _('search'),
+ name: 'query',
+ triggerAction: 'all',
+ clearBtnCls: 'x-field-search-clear',
+ searchBtnCls: 'x-field-search-go',
+ onTrigger1Click: this._triggerSearch,
+ onTrigger2Click: this._triggerClear,
+ });
+ FileMan.combo.Search.superclass.constructor.call(this, config);
+ this.on('render', function () {
+ this.getEl().addKeyListener(Ext.EventObject.ENTER, function () {
+ this._triggerSearch();
+ }, this);
+ });
+ this.addEvents('clear', 'search');
+};
+Ext.extend(FileMan.combo.Search, Ext.form.TwinTriggerField, {
+
+ initComponent: function () {
+ Ext.form.TwinTriggerField.superclass.initComponent.call(this);
+ this.triggerConfig = {
+ tag: 'span',
+ cls: 'x-field-search-btns',
+ cn: [
+ {tag: 'div', cls: 'x-form-trigger ' + this.searchBtnCls},
+ {tag: 'div', cls: 'x-form-trigger ' + this.clearBtnCls}
+ ]
+ };
+ },
+
+ _triggerSearch: function () {
+ this.fireEvent('search', this);
+ },
+
+ _triggerClear: function () {
+ this.fireEvent('clear', this);
+ },
+
+});
+Ext.reg('fileman-combo-search', FileMan.combo.Search);
+Ext.reg('fileman-field-search', FileMan.combo.Search);
\ No newline at end of file
diff --git a/assets/components/fileman/js/mgr/misc/utils.js b/assets/components/fileman/js/mgr/misc/utils.js
new file mode 100644
index 0000000..19f667c
--- /dev/null
+++ b/assets/components/fileman/js/mgr/misc/utils.js
@@ -0,0 +1,105 @@
+FileMan.utils.renderBoolean = function (value) {
+ return value
+ ? String.format('{0} ', _('yes'))
+ : String.format('{0} ', _('no'));
+};
+
+// Helper title render
+FileMan.utils.renderName = function (value, props, row) {
+ return value + ( row.data['description'] ? '' + row.data['description'] + ' ' : '');
+}
+
+
+FileMan.utils.getMenu = function (actions, grid, selected) {
+ const menu = [];
+ let cls, icon, title, action;
+
+ let has_delete = false;
+ for (const i in actions) {
+ if (!actions.hasOwnProperty(i)) {
+ continue;
+ }
+
+ const a = actions[i];
+ if (!a['menu']) {
+ if (a == '-') {
+ menu.push('-');
+ }
+ continue;
+ }
+ else if (menu.length > 0 && !has_delete && (/^remove/i.test(a['action']) || /^delete/i.test(a['action']))) {
+ menu.push('-');
+ has_delete = true;
+ }
+
+ if (selected.length > 1) {
+ if (!a['multiple']) {
+ continue;
+ }
+ else if (typeof(a['multiple']) == 'string') {
+ a['title'] = a['multiple'];
+ }
+ }
+
+ icon = a['icon'] ? a['icon'] : '';
+ if (typeof(a['cls']) == 'object') {
+ if (typeof(a['cls']['menu']) != 'undefined') {
+ icon += ' ' + a['cls']['menu'];
+ }
+ }
+ else {
+ cls = a['cls'] ? a['cls'] : '';
+ }
+ title = a['title'] ? a['title'] : a['title'];
+ action = a['action'] ? grid[a['action']] : '';
+
+ menu.push({
+ handler: action,
+ text: String.format(
+ '{2} ',
+ cls, icon, title
+ ),
+ scope: grid
+ });
+ }
+
+ return menu;
+};
+
+FileMan.utils.renderActions = function (value, props, row) {
+ const res = [];
+ let cls, icon, title, action, item;
+ for (const i in row.data.actions) {
+ if (!row.data.actions.hasOwnProperty(i)) {
+ continue;
+ }
+ const a = row.data.actions[i];
+ if (!a['button']) {
+ continue;
+ }
+
+ icon = a['icon'] ? a['icon'] : '';
+ if (typeof(a['cls']) == 'object') {
+ if (typeof(a['cls']['button']) != 'undefined') {
+ icon += ' ' + a['cls']['button'];
+ }
+ }
+ else {
+ cls = a['cls'] ? a['cls'] : '';
+ }
+ action = a['action'] ? a['action'] : '';
+ title = a['title'] ? a['title'] : '';
+
+ item = String.format(
+ '
',
+ cls, icon, action, title
+ );
+
+ res.push(item);
+ }
+
+ return String.format(
+ '',
+ res.join('')
+ );
+};
\ No newline at end of file
diff --git a/assets/components/fileman/js/mgr/sections/home.js b/assets/components/fileman/js/mgr/sections/home.js
new file mode 100644
index 0000000..d00f0b5
--- /dev/null
+++ b/assets/components/fileman/js/mgr/sections/home.js
@@ -0,0 +1,12 @@
+FileMan.page.Home = function (config) {
+ config = config || {};
+ Ext.applyIf(config, {
+ components: [{
+ xtype: 'fileman-panel-home',
+ renderTo: 'fileman-panel-home-div'
+ }]
+ });
+ FileMan.page.Home.superclass.constructor.call(this, config);
+};
+Ext.extend(FileMan.page.Home, MODx.Component);
+Ext.reg('fileman-page-home', FileMan.page.Home);
\ No newline at end of file
diff --git a/assets/components/fileman/js/mgr/widgets/files.grid.js b/assets/components/fileman/js/mgr/widgets/files.grid.js
new file mode 100644
index 0000000..08aeb18
--- /dev/null
+++ b/assets/components/fileman/js/mgr/widgets/files.grid.js
@@ -0,0 +1,625 @@
+FileMan.grid.Files = function (config) {
+ config = config || {};
+ if (!config.id) {
+ config.id = 'fileman-grid-files';
+ }
+ this.sm = new Ext.grid.CheckboxSelectionModel();
+ Ext.applyIf(config, {
+ url: FileMan.config.connectorUrl,
+ fields: FileMan.config.file_fields,
+ columns: this.getColumns(config),
+ //grouping: true,
+ ddText: _('fileman_ddtext'),
+ tbar: this.getTopBar(config),
+ sm: this.sm,
+ baseParams: {
+ action: 'File\\GetList',
+ resource_id: FileMan.config.resource_id
+ },
+ listeners: {
+ rowDblClick: function (grid, rowIndex, e) {
+ var row = grid.store.getAt(rowIndex);
+ this.updateFile(grid, e, row);
+ },
+ sortchange: this.saveSort
+ },
+ viewConfig: {
+ forceFit: true,
+ enableRowBody: true,
+ autoFill: true,
+ showPreview: true,
+ scrollOffset: 0
+ },
+ paging: true,
+ remoteSort: true,
+ autoHeight: true
+ });
+
+ // Enable D&D only in resource editor
+ if (FileMan.config.resource_id)
+ Ext.applyIf(config, {
+ plugins: [new Ext.ux.dd.GridDragDropRowOrder({
+ copy: false,
+ scrollable: true,
+ targetCfg: {},
+ listeners: {
+ 'afterrowmove': { fn: this.onAfterRowMove, scope: this }
+ }
+ })]
+ });
+
+ // Restore sort state
+ var sortInfo = [];
+ if (sortInfo = this.restoreSort()) {
+ // Workaround for absence of sortInfo support
+ config.baseParams.sort = sortInfo[0];
+ config.baseParams.dir = sortInfo[1];
+
+ config.sortBy = sortInfo[0];
+ config.sortDir = sortInfo[1];
+ }
+
+ FileMan.grid.Files.superclass.constructor.call(this, config);
+
+ // Set sort arrow
+ if (sortInfo.length > 0)
+ this.store.setDefaultSort(sortInfo[0], sortInfo[1]);
+
+ this.restoreColumn();
+ this.colModel.on('hiddenchange', this.saveColumn, this);
+
+ // Clear selection on grid refresh
+ this.store.on('load', function () {
+ if (this._getSelectedIds().length) {
+ this.getSelectionModel().clearSelections();
+ }
+ }, this);
+}
+Ext.extend(FileMan.grid.Files, MODx.grid.Grid, {
+ windows: {
+ UploadByUrl: false
+ },
+
+ // File context menu
+ getMenu: function (grid, rowIndex) {
+ var menu = [
+ { handler: grid['updateFile'], text: '' + _('fileman_update') },
+ { handler: grid['downloadFile'], text: '' + _('file_download') },
+ '-',
+ { handler: grid['resetFileDownloads'], text: '' + _('fileman_reset_downloads') },
+ '-',
+ { handler: grid['removeFile'], text: '' + _('remove') }
+ ];
+
+ this.addContextMenuItem(menu);
+ },
+
+ // Restore sort info to session storage
+ restoreSort: function () {
+ if (typeof (Storage) !== "undefined") {
+ var sortInfo = sessionStorage.getItem('fa_sort' + ((FileMan.config.resource_id > 0) ? '_' + FileMan.config.resource_id : ''));
+ return (sortInfo) ? sortInfo.split('|', 2) : false;
+ }
+
+ return false;
+ },
+
+ // Save sort info to session storage
+ saveSort: function (grid, sortInfo) {
+ if (typeof (Storage) !== "undefined") {
+ sessionStorage.setItem('fa_sort' + ((FileMan.config.resource_id > 0) ? '_' + FileMan.config.resource_id : ''),
+ sortInfo.field + "|" + sortInfo.direction);
+ }
+ },
+
+ // Restore column info from session storage
+ restoreColumn: function () {
+ if (typeof (Storage) !== "undefined") {
+ var colInfo = sessionStorage.getItem('fa_col' + ((FileMan.config.resource_id > 0) ? '_' + FileMan.config.resource_id : ''));
+ if (colInfo != null) {
+ var cols = colInfo.split(',');
+ for (var i = 0; i < cols.length; i++)
+ this.colModel.setHidden(i + 1, cols[i] == '0');
+ }
+ }
+ },
+
+ // Save column visibility to session storage
+ saveColumn: function (colModel, colIndex, hidden) {
+ if (typeof (Storage) !== "undefined") {
+ var count = colModel.getColumnCount(false);
+ var cols = [];
+ for (var i = 1; i < count; i++) cols.push(colModel.isHidden(i) ? 0 : 1);
+
+ sessionStorage.setItem('fa_col' + ((FileMan.config.resource_id > 0) ? '_' + FileMan.config.resource_id : ''),
+ cols.join(','));
+ }
+ },
+
+ // Edit File
+ updateFile: function (btn, e, row) {
+ if (typeof (row) != 'undefined') {
+ this.menu.record = row.data;
+ }
+ else if (!this.menu.record) {
+ return false;
+ }
+ var id = this.menu.record.id;
+
+ MODx.Ajax.request({
+ url: this.config.url,
+ params: {
+ action: 'File\\Get',
+ resource_id: FileMan.config.resource_id,
+ id: id
+ },
+ listeners: {
+ success: {
+ fn: function (r) {
+ var w = MODx.load({
+ xtype: 'fileman-file-window-update',
+ id: Ext.id(),
+ record: r,
+ listeners: {
+ success: {
+ fn: function () {
+ this.refresh();
+ }, scope: this
+ }
+ }
+ });
+ w.reset();
+ w.setValues(r.object);
+ w.show(e.target);
+ }, scope: this
+ }
+ }
+ });
+ },
+
+ // Edit file access
+ accessFile: function (act, btn, e) {
+ var ids = this._getSelectedIds();
+ if (!ids.length) {
+ return false;
+ }
+
+ MODx.Ajax.request({
+ url: this.config.url,
+ params: {
+ action: 'File\\Access',
+ private: (act.name == 'close') ? 1 : 0,
+ ids: Ext.util.JSON.encode(ids)
+ },
+ listeners: {
+ success: {
+ fn: function (r) {
+ this.refresh();
+ }, scope: this
+ },
+ failure: {
+ fn: function (r) {
+ }, scope: this
+ }
+ }
+ });
+
+ return true;
+ },
+
+ // Reset download count
+ resetFileDownloads: function (act, btn, e) {
+ var ids = this._getSelectedIds();
+ if (!ids.length) {
+ return false;
+ }
+ MODx.msg.confirm({
+ title: ids.length > 1
+ ? _('reset_downloads')
+ : _('reset_downloads'),
+ text: ids.length > 1
+ ? _('fileman_resets_downloads_confirm')
+ : _('fileman_reset_downloads_confirm'),
+ url: this.config.url,
+ params: {
+ action: 'File\\Reset',
+ ids: Ext.util.JSON.encode(ids)
+ },
+ listeners: {
+ success: {
+ fn: function (r) {
+ this.refresh();
+ }, scope: this
+ }
+ }
+ });
+ return true;
+ },
+
+ // Remove file
+ removeFile: function (act, btn, e) {
+ var ids = this._getSelectedIds();
+ if (!ids.length) {
+ return false;
+ }
+ MODx.msg.confirm({
+ title: ids.length > 1
+ ? _('remove')
+ : _('remove'),
+ text: ids.length > 1
+ ? _('confirm_remove')
+ : _('confirm_remove'),
+ url: this.config.url,
+ params: {
+ action: 'File\\Remove',
+ ids: Ext.util.JSON.encode(ids)
+ },
+ listeners: {
+ success: {
+ fn: function (r) {
+ this.refresh();
+ }, scope: this
+ }
+ }
+ });
+ return true;
+ },
+
+ // Download file
+ downloadFile: function (act, btn, e) {
+ var item = this._getSelected();
+
+ var filePath = item['path'] + item['internal_name'];
+
+ MODx.Ajax.request({
+ url: MODx.config.connector_url,
+ params: {
+ // TODO:
+ //action: 'File\\Update',
+ action: 'Browser/File/Download',
+ file: filePath,
+ wctx: MODx.ctx || '',
+ source: MODx.config['fileman_mediasource']
+ },
+ listeners: {
+ 'success': {
+ fn: function (r) {
+ if (!Ext.isEmpty(r.object.url)) {
+ location.href = MODx.config.connector_url +
+ '?action=Browser/File/Download&download=1&file=' +
+ filePath + '&HTTP_MODAUTH=' + MODx.siteId +
+ '&source=' + MODx.config['fileman_mediasource'] + '&wctx=' + MODx.ctx;
+ }
+ }, scope: this
+ }
+ }
+ });
+
+ return true;
+ },
+
+ // Show uploader dialog
+ uploadFiles: function (btn, e) {
+ if (!this.uploader) {
+ this.uploader = new MODx.util.MultiUploadDialog.Dialog({
+ title: _('upload'),
+ url: this.config.url,
+ base_params: {
+ action: 'File\\Upload',
+ resource_id: FileMan.config.resource_id
+ },
+ cls: 'modx-upload-window'
+ });
+ this.uploader.on('hide', this.refresh, this);
+ this.uploader.on('close', this.refresh, this);
+ }
+
+ // Automatically open picker
+ this.uploader.show(btn);
+ },
+
+ // Define columns
+ getColumns: function (config) {
+ var columnsRaw = {
+ id: { sortable: true, width: 40 },
+ name: { sortable: true, width: 120 },
+ title: { sortable: true, width: 250, renderer: FileMan.utils.renderName },
+ group: { sortable: true, width: 150 },
+ extension: { sortable: true, width: 50 },
+ download: { sortable: true, width: 50 },
+ private: { sortable: true, width: 50, renderer: FileMan.utils.renderBoolean }
+ };
+
+ var columns = [this.sm];
+ if (FileMan.config.resource_id) {
+ columns.push({
+ header: _('fileman_sort_order'),
+ dataIndex: 'sort_order',
+ hidden: false,
+ sortable: FileMan.config.resource_id > 0,
+ width: 40
+ });
+ }
+
+ for (var i = 0; i < FileMan.config.files_grid_fields.length; i++) {
+ var column = FileMan.config.files_grid_fields[i];
+ if (columnsRaw[column]) {
+ Ext.applyIf(columnsRaw[column], {
+ header: _('fileman_' + column),
+ dataIndex: column
+ });
+ columns.push(columnsRaw[column]);
+ }
+ }
+
+ if (!FileMan.config.resource_id)
+ columns.push({
+ header: _('resource'),
+ dataIndex: 'pagetitle',
+ sortable: true
+ }, {
+ header: _('user'),
+ dataIndex: 'username',
+ sortable: true
+ });
+
+ return columns;
+ },
+
+ // Form top bar
+ getTopBar: function (config) {
+ var fields = [];
+
+ if (FileMan.config.resource_id)
+ fields.push({
+ xtype: 'button',
+ cls: 'primary-button',
+ text: _('fileman_btn_upload'),
+ handler: this.uploadFiles,
+ scope: this
+ }, {
+ xtype: 'button',
+ cls: '',
+ text: _('fileman_btn_upload_by_url'),
+ tooltip: _('fileman_btn_upload_by_url_tooltip'),
+ scope: this,
+ handler: this.showUploadByUrlWindow
+ });
+
+ fields.push({
+ xtype: 'splitbutton',
+ text: _('remove'),
+ menu: [
+ {
+ name: 'open',
+ text: _('fileman_open'),
+ handler: this.accessFile,
+ scope: this
+ },
+ {
+ name: 'close',
+ text: _('fileman_private'),
+ handler: this.accessFile,
+ scope: this
+ },
+ '-',
+ {
+ text: _('fileman_reset_downloads'),
+ handler: this.resetFileDownloads,
+ scope: this
+ },
+ {
+ text: _('remove'),
+ handler: this.removeFile,
+ scope: this
+ }
+ ],
+ text: _('bulk_actions')
+ }, '->', {
+ xtype: 'textfield',
+ name: 'user',
+ width: 200,
+ id: config.id + '-search-user-field',
+ emptyText: _('user'),
+ listeners: {
+ render: {
+ fn: function (tf) {
+ tf.getEl().addKeyListener(Ext.EventObject.ENTER,
+ function () {
+ this._doSearch(tf);
+ }, this);
+ }, scope: this
+ }
+ }
+ }, {
+ xtype: 'textfield',
+ name: 'query',
+ width: 200,
+ id: config.id + '-search-field',
+ emptyText: _('search'),
+ listeners: {
+ render: {
+ fn: function (tf) {
+ tf.getEl().addKeyListener(Ext.EventObject.ENTER,
+ function () {
+ this._doSearch(tf);
+ }, this);
+ }, scope: this
+ }
+ }
+ }, {
+ xtype: 'button',
+ id: config.id + '-search-clear',
+ text: ' ',
+ listeners: {
+ click: { fn: this._clearSearch, scope: this }
+ }
+ });
+
+ return fields;
+ },
+
+ showUploadByUrlWindow: function () {
+ if (this.windows.UploadByUrl) {
+ this.windows.UploadByUrl.destroy()
+ }
+
+ var config = {
+ xtype: 'fileman-window-upload-by-url',
+ class_id: this.config.id,
+ id: this.config.id + '-window-upload-by-url'
+ };
+
+ this.windows.UploadByUrl = MODx.load(config);
+ this.windows.UploadByUrl.show(Ext.EventObject.target);
+ },
+
+
+ uploadByUrl: function (record, callback) {
+
+ var $this = this;
+ if (record.url === undefined) {
+ MODx.msg.alert(_('error'), _('msgs_empty_url'));
+ return false;
+ }
+
+ MODx.Ajax.request({
+ url: FileMan.config.connectorUrl,
+ params: {
+ action: 'File\\Upload',
+ resource_id: FileMan.config.resource_id,
+ url: record.url,
+ title: record.title
+ },
+ listeners: {
+ success: {
+ fn: function (r) {
+ if (r.success) {
+ if (typeof callback === 'function') {
+ callback(r);
+ }
+ // TODO: галочку "не закрывать"
+ if (this.windows.UploadByUrl) {
+ this.windows.UploadByUrl.destroy()
+ }
+
+ this.refresh();
+ }
+ }
+ , scope: this
+ }
+ , failure: {
+ fn: function (r) {
+ $this.error = true;
+ if (typeof callback === 'function') {
+ callback(r);
+ }
+ MODx.msg.alert(_('error'), r.message);
+ }
+ , scope: this
+ }
+ }
+ });
+ return true;
+ },
+
+ // Header button handler
+ onClick: function (e) {
+ var elem = e.getTarget();
+ if (elem.nodeName == 'BUTTON') {
+ var row = this.getSelectionModel().getSelected();
+ if (typeof (row) != 'undefined') {
+ var action = elem.getAttribute('action');
+ if (action == 'showMenu') {
+ var ri = this.getStore().find('id', row.id);
+ return this._showMenu(this, ri, e);
+ }
+ else if (typeof this[action] === 'function') {
+ this.menu.record = row.data;
+ return this[action](this, e);
+ }
+ }
+ }
+ return this.processEvent('click', e);
+ },
+
+ // Get first selected record
+ _getSelected: function () {
+ var selected = this.getSelectionModel().getSelections();
+
+ for (var i in selected) {
+ if (!selected.hasOwnProperty(i)) continue;
+ return selected[i].json;
+ }
+
+ return null;
+ },
+
+ // Get list of selected ID
+ _getSelectedIds: function () {
+ var ids = [];
+ var selected = this.getSelectionModel().getSelections();
+
+ for (var i in selected) {
+ if (!selected.hasOwnProperty(i)) continue;
+ ids.push(selected[i]['id']);
+ }
+
+ return ids;
+ },
+
+ // Perform store update with search query
+ _doSearch: function (tf, nv, ov) {
+ if (tf.name == 'query')
+ this.getStore().baseParams.query = tf.getValue();
+
+ if (tf.name == 'user')
+ this.getStore().baseParams.user = tf.getValue();
+ this.getBottomToolbar().changePage(1);
+ this.refresh();
+ },
+
+ // Reset search query
+ _clearSearch: function (btn, e) {
+ this.getStore().baseParams.query = '';
+ this.getStore().baseParams.user = '';
+ Ext.getCmp(this.config.id + '-search-user-field').setValue('');
+ Ext.getCmp(this.config.id + '-search-field').setValue('');
+ this.getBottomToolbar().changePage(1);
+ this.refresh();
+ },
+
+ // Handle changing file order with dragging
+ onAfterRowMove: function (dt, sri, ri, sels) {
+ var s = this.getStore();
+ var sourceRec = s.getAt(sri);
+ var belowRec = s.getAt(ri);
+ var total = s.getTotalCount();
+ var upd = {};
+
+ sourceRec.set('sort_order', sri);
+ sourceRec.commit();
+ upd[sourceRec.get('id')] = sri;
+
+ var brec;
+ for (var x = (ri - 1); x < total; x++) {
+ brec = s.getAt(x);
+ if (brec) {
+ brec.set('sort_order', x);
+ brec.commit();
+ upd[brec.get('id')] = x;
+ }
+ }
+
+ MODx.Ajax.request({
+ url: this.config.url,
+ params: {
+ action: 'File\\Sort',
+ sort_order: Ext.util.JSON.encode(upd)
+ }
+ });
+
+ return true;
+ }
+});
+Ext.reg('fileman-grid-files', FileMan.grid.Files);
\ No newline at end of file
diff --git a/assets/components/fileman/js/mgr/widgets/files.window.js b/assets/components/fileman/js/mgr/widgets/files.window.js
new file mode 100644
index 0000000..e9a8326
--- /dev/null
+++ b/assets/components/fileman/js/mgr/widgets/files.window.js
@@ -0,0 +1,215 @@
+FileMan.window.UpdateFile = function (config) {
+ config = config || {};
+ if (!config.id) {
+ config.id = 'fileman-file-window-update';
+ }
+ Ext.applyIf(config, {
+ title: _('update'),
+ bwrapCssClass: 'x-window-with-tabs',
+ width: 550,
+ autoHeight: true,
+ url: FileMan.config.connectorUrl,
+ action: 'File\\Update',
+ fields: this.getFields(config),
+ keys: [
+ {
+ key: Ext.EventObject.ENTER, shift: true, fn: function () {
+ this.submit()
+ }, scope: this
+ }
+ ]
+ });
+
+ FileMan.window.UpdateFile.superclass.constructor.call(this, config);
+}
+
+Ext.extend(FileMan.window.UpdateFile, MODx.Window, {
+ calcHash: function (btn, e, row) {
+ btn.hide();
+ MODx.Ajax.request({
+ url: this.config.url,
+ params: {
+ action: 'File\\Hash',
+ id: this.config.record.object.id
+ },
+ listeners: {
+ success: {
+ fn: function (r) {
+ Ext.getCmp(this.config.id + '-hash').setValue(r.object.hash);
+ Ext.getCmp(this.config.id + '-hash').show();
+ }, scope: this
+ },
+ fail: {
+ fn: function () {
+ btn.show();
+ }, scope: this
+ }
+ }
+ });
+ },
+
+ getFields: function (config) {
+ var fieldsTabGeneral = [
+ {
+ xtype: 'hidden',
+ name: 'id',
+ id: config.id + '-id'
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: _('fileman_title'),
+ name: 'title',
+ id: config.id + '-title',
+ anchor: '99%',
+ allowBlank: true
+ },
+ {
+ xtype: 'textarea',
+ fieldLabel: _('fileman_description'),
+ name: 'description',
+ id: config.id + '-description',
+ anchor: '99%',
+ height: 120
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: _('fileman_group'),
+ name: 'group',
+ id: config.id + '-group',
+ anchor: '99%',
+ allowBlank: true
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: _('fileman_name'),
+ name: 'name',
+ id: config.id + '-name',
+ anchor: '99%',
+ allowBlank: false
+ },
+ {
+ xtype: 'xcheckbox',
+ id: config.id + '-private',
+ boxLabel: _('fileman_private'),
+ hideLabel: true,
+ name: 'private'
+ }
+ ];
+
+ var fieldsTabSettings = [
+ {
+ xtype: 'statictextfield',
+ fieldLabel: _('fileman_path'),
+ name: 'path',
+ id: config.id + '-path',
+ anchor: '99%'
+ },
+ {
+ xtype: 'statictextfield',
+ fieldLabel: _('fileman_internal_name'),
+ name: 'internal_name',
+ id: config.id + '-internal_name',
+ anchor: '99%'
+ },
+ {
+ xtype: 'statictextfield',
+ fieldLabel: _('fileman_extension'),
+ name: 'extension',
+ id: config.id + '-extension',
+ anchor: '99%'
+ },
+ {
+ xtype: 'statictextfield',
+ fieldLabel: _('fileman_fid'),
+ name: 'fid',
+ id: config.id + '-fid',
+ anchor: '99%'
+ },
+ {
+ xtype: 'statictextfield',
+ fieldLabel: _('fileman_hash'),
+ id: config.id + '-hash',
+ name: 'hash',
+ //hidden: (config.record.object.hash == ''),
+ anchor: '99%'
+ }
+ ];
+
+ if (config.record.object.hash == '') {
+ fieldsTabSettings.push([
+ {
+ xtype: 'button',
+ text: _('fileman_calculate'),
+ handler: this.calcHash,
+ scope: this
+ }
+ ]);
+ }
+
+ //return fields;
+
+ var result = [];
+ if (FileMan.config.resource_id > 0) {
+ result.push({ xtype: 'hidden', name: 'resource_id', id: config.id + '-resource_id' });
+ }
+ else {
+ fieldsTabSettings.unshift({
+ xtype: 'modx-combo',
+ id: config.id + '-resource_id',
+ fieldLabel: _('resource'),
+ name: 'resource_id',
+ hiddenName: 'resource_id',
+ url: FileMan.config.connectorUrl,
+ baseParams: {
+ action: 'Resource\\Combo'
+ },
+ fields: ['id', 'pagetitle', 'description'],
+ displayField: 'pagetitle',
+ anchor: '99%',
+ pageSize: 10,
+ editable: true,
+ typeAhead: true,
+ allowBlank: false,
+ forceSelection: true,
+ tpl: new Ext.XTemplate('{pagetitle} ',
+ '{description} ', '
')
+ });
+ }
+
+ var tabs = [
+ {
+ title: _('fileman_file_tab_general'),
+ layout: 'anchor',
+ items: [
+ {
+ layout: 'form',
+ cls: 'modx-panel',
+ items: [fieldsTabGeneral]
+ }
+ ]
+ },
+ {
+ title: _('fileman_file_tab_settings'),
+ layout: 'anchor',
+ items: [
+ {
+ layout: 'form',
+ cls: 'modx-panel',
+ items: [fieldsTabSettings]
+ }
+ ]
+ }
+ ];
+
+ result.push({
+ xtype: 'modx-tabs',
+ defaults: { border: false, autoHeight: true },
+ deferredRender: false,
+ border: true,
+ hideMode: 'offsets',
+ items: [tabs]
+ });
+ return result;
+ }
+});
+Ext.reg('fileman-file-window-update', FileMan.window.UpdateFile);
\ No newline at end of file
diff --git a/assets/components/fileman/js/mgr/widgets/home.panel.js b/assets/components/fileman/js/mgr/widgets/home.panel.js
new file mode 100644
index 0000000..8728546
--- /dev/null
+++ b/assets/components/fileman/js/mgr/widgets/home.panel.js
@@ -0,0 +1,37 @@
+FileMan.panel.Home = function (config) {
+ config = config || {};
+ Ext.apply(config, {
+ baseCls: 'modx-formpanel',
+ layout: 'anchor',
+ /*
+ stateful: true,
+ stateId: 'fileman-panel-home',
+ stateEvents: ['tabchange'],
+ getState:function() {return {activeTab:this.items.indexOf(this.getActiveTab())};},
+ */
+ hideMode: 'offsets',
+ items: [{
+ xtype: 'modx-header',
+ html: _('fileman')
+ }, {
+ xtype: 'modx-tabs',
+ defaults: {border: false, autoHeight: true},
+ border: true,
+ hideMode: 'offsets',
+ items: [{
+ title: _('fileman_files'),
+ layout: 'anchor',
+ items: [{
+ html: _('fileman_intro_msg'),
+ cls: 'panel-desc',
+ }, {
+ xtype: 'fileman-grid-files',
+ cls: 'main-wrapper',
+ }]
+ }]
+ }]
+ });
+ FileMan.panel.Home.superclass.constructor.call(this, config);
+};
+Ext.extend(FileMan.panel.Home, MODx.Panel);
+Ext.reg('fileman-panel-home', FileMan.panel.Home);
diff --git a/assets/components/fileman/js/mgr/widgets/page.panel.js b/assets/components/fileman/js/mgr/widgets/page.panel.js
new file mode 100644
index 0000000..d89d74d
--- /dev/null
+++ b/assets/components/fileman/js/mgr/widgets/page.panel.js
@@ -0,0 +1,18 @@
+FileMan.panel.Page = function (config) {
+ config = config || {};
+ Ext.apply(config, {
+ baseCls: 'modx-formpanel',
+ layout: 'anchor',
+
+ hideMode: 'offsets',
+ items: [
+ {
+ xtype: 'fileman-grid-files',
+ cls: 'main-wrapper',
+ record: config.record
+ }]
+ });
+ FileMan.panel.Page.superclass.constructor.call(this, config);
+};
+Ext.extend(FileMan.panel.Page, MODx.Panel);
+Ext.reg('fileman-panel-page', FileMan.panel.Page);
\ No newline at end of file
diff --git a/assets/components/fileman/js/mgr/widgets/upload_by_url.window.js b/assets/components/fileman/js/mgr/widgets/upload_by_url.window.js
new file mode 100644
index 0000000..c5df94d
--- /dev/null
+++ b/assets/components/fileman/js/mgr/widgets/upload_by_url.window.js
@@ -0,0 +1,169 @@
+FileMan.window.UploadByUrl = function (config) {
+ config = config || {}
+
+ Ext.applyIf(config, {
+ title: _('fileman_window_upload_by_url_title'),
+ url: FileMan.config['connector_url'],
+ cls: 'modx-window fileman-window ' || config['cls'],
+ width: 900,
+ autoHeight: true,
+ allowDrop: false,
+ record: {},
+ baseParams: {},
+ fields: [
+ {
+ layout: 'column',
+ items: [{
+ columnWidth: .7,
+ layout: 'form',
+ defaults: {msgTarget: 'under'},
+ items: [
+ {
+ xtype: 'textfield',
+ id: config.id + '-url',
+ fieldLabel: _('fileman_field_url'),
+ name: 'url',
+ //value: 'http://www.pk-tp.ru/administrator/templates/bluestork/images/logo.png',
+ value: '',
+ anchor: '99%',
+ allowBlank: false,
+ defaultAutoCreate: {
+ tag: 'input',
+ type: 'text',
+ size: '16',
+ autocomplete: 'off'
+ }
+ },{
+ xtype: 'textfield',
+ id: config.id + '-title',
+ fieldLabel: _('fileman_field_title'),
+ name: 'title',
+ value: '',
+ anchor: '99%',
+ allowBlank: true
+ }, {
+ xtype: 'modx-description',
+ cls: 'fileman-window-url-description',
+ html: _('fileman_url_description'),
+ id: config.id + '-description'
+ }, {
+ xtype: 'xcheckbox',
+ boxLabel: _('fileman_url_close_window'),
+ hideLabel: true,
+ name: 'close',
+ id: config.id + '-close'
+ }
+ ]
+ }, {
+ columnWidth: .3,
+ layout: 'form',
+ defaults: {msgTarget: 'under'},
+ items: [{
+ xtype: 'displayfield',
+ name: 'image',
+ cls: 'fileman-preview',
+ value: ' ',
+ id: config.id + '-preview',
+ anchor: '99%',
+ allowBlank: false,
+ scope: this,
+ renderer: this.renderPreview
+ }]
+ }]
+ }
+ ],
+ keys: this.getKeys(config),
+ buttons: this.getButtons(config),
+ listeners: this.getListeners(config)
+ })
+ FileMan.window.UploadByUrl.superclass.constructor.call(this, config)
+
+ this.on('hide', function () {
+ var w = this
+ window.setTimeout(function () {
+ w.close()
+ }, 200)
+ })
+
+ this.on('afterrender', function () {
+ var fbDom = Ext.get(config.id)
+ fbDom.addListener('keydown', function () {
+ this.renderPreview(config)
+ }, this);
+
+ fbDom.addListener('keyup', function () {
+ this.renderPreview(config)
+ }, this);
+ })
+
+}
+Ext.extend(FileMan.window.UploadByUrl, MODx.Window, {
+ renderPreview: function (config) {
+ window.setTimeout(function () {
+ var newValue = Ext.getCmp(config.id + '-url').getValue();
+ var elem = Ext.getCmp(config.id + '-preview');
+
+ var extension = newValue.split('.').pop().toLowerCase();
+ var iconsPreview = ['pdf', 'doc', 'docx', 'xls', 'xlsx'];
+ if(iconsPreview.indexOf(extension) !== -1) {
+ elem.setValue(' ')
+ } else {
+ elem.setValue(' ')
+ }
+ }, 200)
+ },
+
+
+
+ getButtons: function (config) {
+ return [{
+ text: config.cancelBtnText || _('cancel'),
+ scope: this,
+ handler: function () {
+ config.closeAction !== 'close'
+ ? this.hide()
+ : this.close()
+ }
+ }, {
+ text: _('upload'),
+ cls: 'primary-button',
+ scope: this,
+ handler: function () {
+ var values = this.fp.getForm().getValues();
+ var el = this.getEl();
+ el.mask(_('loading'), 'x-mask-loading');
+ FileMan.typeLoad = 'url';
+ Ext.getCmp(this.class_id).uploadByUrl({
+ url: values.url,
+ title: values.title
+ }, function (response) {
+ el.unmask();
+ })
+ }
+ }]
+ },
+ getKeys: function () {
+ return [{
+ key: Ext.EventObject.ENTER,
+ shift: true,
+ fn: function () {
+ var values = this.fp.getForm().getValues()
+ FileMan.typeLoad = 'url';
+ FileMan.uploadByUrl({
+ url: values.url,
+ title: values.title
+ })
+ }, scope: this
+ }]
+ },
+ getListeners: function (config) {
+ return {
+ success: {
+ fn: function () {
+ this.refresh()
+ }, scope: this
+ }
+ }
+ }
+});
+Ext.reg('fileman-window-upload-by-url', FileMan.window.UploadByUrl);
\ No newline at end of file
diff --git a/core/components/fileman/bootstrap.php b/core/components/fileman/bootstrap.php
new file mode 100644
index 0000000..0bcc31c
--- /dev/null
+++ b/core/components/fileman/bootstrap.php
@@ -0,0 +1,13 @@
+addPackage('FileMan\Model', $namespace['path'] . 'src/', null, 'FileMan\\');
+
+$modx->services->add('FileMan', function ($c) use ($modx) {
+ return new FileMan\FileMan($modx);
+});
diff --git a/core/components/fileman/controllers/home.class.php b/core/components/fileman/controllers/home.class.php
new file mode 100644
index 0000000..c6d5914
--- /dev/null
+++ b/core/components/fileman/controllers/home.class.php
@@ -0,0 +1,82 @@
+FileMan = $this->modx->services->get('FileMan');
+ parent::initialize();
+ }
+
+
+ /**
+ * @return array
+ */
+ public function getLanguageTopics()
+ {
+ return ['fileman:default'];
+ }
+
+
+ /**
+ * @return bool
+ */
+ public function checkPermissions()
+ {
+ return true;
+ }
+
+
+ /**
+ * @return null|string
+ */
+ public function getPageTitle()
+ {
+ return $this->modx->lexicon('fileman');
+ }
+
+
+ /**
+ * @return void
+ */
+ public function loadCustomCssJs()
+ {
+ $this->addCss($this->FileMan->config['cssUrl'] . 'mgr/main.css');
+ $this->addJavascript($this->FileMan->config['jsUrl'] . 'mgr/fileman.js');
+ $this->addJavascript($this->FileMan->config['jsUrl'] . 'mgr/misc/utils.js');
+ $this->addJavascript($this->FileMan->config['jsUrl'] . 'mgr/misc/combo.js');
+ $this->addJavascript($this->FileMan->config['jsUrl'] . 'mgr/widgets/files.grid.js');
+ $this->addJavascript($this->FileMan->config['jsUrl'] . 'mgr/widgets/files.window.js');
+ $this->addJavascript($this->FileMan->config['jsUrl'] . 'mgr/widgets/home.panel.js');
+ $this->addJavascript($this->FileMan->config['jsUrl'] . 'mgr/sections/home.js');
+
+ $this->addHtml('');
+ }
+
+
+ /**
+ * @return string
+ */
+ public function getTemplateFile()
+ {
+ $this->content .= '
';
+ return '';
+ }
+}
diff --git a/core/components/fileman/docs/changelog.txt b/core/components/fileman/docs/changelog.txt
new file mode 100644
index 0000000..8e83881
--- /dev/null
+++ b/core/components/fileman/docs/changelog.txt
@@ -0,0 +1,7 @@
+Changelog for FileMan.
+
+3.0.0-beta
+==============
+- First version of the FileMan component
+- Requires MODX 3.
+- Requires PHP 7.2+.
\ No newline at end of file
diff --git a/core/components/fileman/docs/license.txt b/core/components/fileman/docs/license.txt
new file mode 100644
index 0000000..3647e8c
--- /dev/null
+++ b/core/components/fileman/docs/license.txt
@@ -0,0 +1,287 @@
+GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+--------------------------
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+Everyone is permitted to copy and distribute verbatim copies
+of this license document, but changing it is not allowed.
+
+Preamble
+--------
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+
+GNU GENERAL PUBLIC LICENSE
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+---------------------------------------------------------------
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+NO WARRANTY
+-----------
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+---------------------------
+END OF TERMS AND CONDITIONS
\ No newline at end of file
diff --git a/core/components/fileman/docs/readme.txt b/core/components/fileman/docs/readme.txt
new file mode 100644
index 0000000..9681b6d
--- /dev/null
+++ b/core/components/fileman/docs/readme.txt
@@ -0,0 +1,5 @@
+--------------------
+FileMan
+--------------------
+Author: Aleksei Naumov
+--------------------
\ No newline at end of file
diff --git a/core/components/fileman/elements/chunks/files.tpl b/core/components/fileman/elements/chunks/files.tpl
new file mode 100644
index 0000000..a640e99
--- /dev/null
+++ b/core/components/fileman/elements/chunks/files.tpl
@@ -0,0 +1,11 @@
+{set $lastGroup = ''}
+{foreach $files as $file}
+ {if $file['group'] != $lastGroup}
+ {$file['group']}
+ {/if}
+
+ {$file['title'] ?: $file['name']}
+ {if $file['description']}{$file['description']} {/if}
+
+ {set $lastGroup = $file['group']}
+{/foreach}
\ No newline at end of file
diff --git a/core/components/fileman/elements/plugins/fileman.php b/core/components/fileman/elements/plugins/fileman.php
new file mode 100644
index 0000000..94e7abf
--- /dev/null
+++ b/core/components/fileman/elements/plugins/fileman.php
@@ -0,0 +1,82 @@
+event->name) {
+ case 'OnDocFormPrerender':
+
+ // Check access
+ if (!$modx->hasPermission('fileman_doclist')) return;
+
+ // Skip form building when resource template is not in permitted list
+ $templates = trim($modx->getOption('fileman_templates'));
+
+ if ($templates != '') {
+ $templates = array_map('trim', explode(',', $templates));
+ $template = $resource->get('template');
+ if (!in_array($template, $templatelist)) {
+ return;
+ }
+ }
+
+ /** @var FileMan $fileMan */
+ $fileMan = new FileMan($modx);
+ $modx->services->add('FileMan', $fileMan);
+
+ $modx->controller->addLexiconTopic('fileman:default');
+
+ $assetsUrl = $fileMan->config['assetsUrl'];
+ $modx->controller->addJavascript($assetsUrl . 'js/mgr/fileman.js');
+ $modx->controller->addLastJavascript($assetsUrl . 'js/mgr/misc/utils.js');
+ $modx->controller->addLastJavascript($assetsUrl . 'js/mgr/misc/combo.js');
+ $modx->controller->addLastJavascript($assetsUrl . 'js/mgr/widgets/files.grid.js');
+ $modx->controller->addLastJavascript($assetsUrl . 'js/mgr/widgets/files.window.js');
+ $modx->controller->addLastJavascript($assetsUrl . 'js/mgr/widgets/upload_by_url.window.js');
+ $modx->controller->addLastJavascript($assetsUrl . 'js/mgr/widgets/page.panel.js');
+
+ $modx->controller->addCss($assetsUrl . 'css/mgr/main.css');
+
+ $fileMan->config['resource_id'] = $resource->get('id');
+
+ $modx->controller->addHtml('');
+
+ $modx->controller->addHtml('
+ ');
+ break;
+
+ // Remove attached files to resources
+ case 'OnEmptyTrash':
+ $fileMan = $modx->services->get('FileMan');
+ if (!$fileMan) {
+ return;
+ }
+
+ foreach ($ids as $id) {
+ $files = $modx->getIterator(\FileMan\Model\File::class, [
+ 'resource_id' => $id
+ ]);
+ foreach ($files as $file) {
+ $file->remove();
+ }
+ }
+
+ break;
+}
diff --git a/core/components/fileman/elements/snippets/files.php b/core/components/fileman/elements/snippets/files.php
new file mode 100644
index 0000000..db890fe
--- /dev/null
+++ b/core/components/fileman/elements/snippets/files.php
@@ -0,0 +1,98 @@
+services->get('FileMan');
+
+// Get script options
+$tpl = $modx->getOption('tpl', $scriptProperties, 'tpl.FileMan.Files');
+
+$sortby = $modx->getOption('sortBy', $scriptProperties, 'sort_order');
+$sortdir = $modx->getOption('sortDir', $scriptProperties, 'ASC');
+$limit = $modx->getOption('limit', $scriptProperties, 0);
+$offset = $modx->getOption('offset', $scriptProperties, 0);
+$totalVar = $modx->getOption('totalVar', $scriptProperties, 'total');
+
+$toPlaceholder = $modx->getOption('toPlaceholder', $scriptProperties, false);
+
+$ids = $modx->getOption('ids', $scriptProperties, '');
+$resource = $modx->getOption('resource', $scriptProperties, 0);
+$showGroups = $modx->getOption('showGroups', $scriptProperties, 1);
+$makeUrl = $modx->getOption('makeUrl', $scriptProperties, true);
+$privateUrl = $modx->getOption('privateUrl', $scriptProperties, false);
+$includeTimeStamp = $modx->getOption('includeTimeStamp', $scriptProperties, false);
+
+
+//
+$mediaSource = $modx->getOption('fileman_mediasource', null, 1);
+$ms = $modx->getObject('sources.modMediaSource', array('id' => $mediaSource));
+$ms->initialize();
+$public_url = $ms->getBaseUrl();
+$private_url = $modx->getOption('fileman_assets_url', null, $modx->getOption('assets_url')) . 'components/fileman/';
+$private_url .= 'download.php?fid=';
+
+// Build query
+$c = $modx->newQuery(File::class);
+
+// resource
+$c->where([
+ 'resource_id' => ($resource > 0) ? $resource : $modx->resource->get('id')
+]);
+
+// ids
+$ids = explode(',', $ids);
+$ids = array_filter(array_map('trim', $ids));
+if(!empty($ids)) {
+ $ids = array_map('intval', $ids);
+ $c->where(['id:IN' => $ids]);
+}
+
+// offset & limit
+if (!empty($limit)) {
+ $total = $modx->getCount(File::class, $c);
+ $modx->setPlaceholder($totalVar, $total);
+ $c->limit($limit, $offset);
+}
+
+// sort
+$c->sortby($sortby, $sortdir);
+
+$items = $modx->getIterator(File::class, $c);
+
+$outputData = [];
+
+/** @var File $item */
+foreach ($items as $item) {
+ $item->source = $ms;
+
+ $itemArr = $item->toArray();
+
+ if ($makeUrl) {
+ if ($itemArr['private'] || $privateUrl) {
+ $itemArr['url'] = $private_url . $itemArr['fid'];
+ }
+ else {
+ $itemArr['url'] = $public_url . $itemArr['path'] . $itemArr['internal_name'];
+ }
+ }
+
+ if ($includeTimeStamp) {
+ $itemArr['timestamp'] = filectime($item->getFullPath());
+ }
+
+ $outputData[] = $itemArr;
+}
+
+// Output
+$output = $fileMan->getChunk($tpl, ['files' => $outputData]);
+
+if (!empty($toPlaceholder)) {
+ // If using a placeholder, output nothing and set output to specified placeholder
+ $modx->setPlaceholder($toPlaceholder, $output);
+ return '';
+}
+
+return $output;
\ No newline at end of file
diff --git a/core/components/fileman/lexicon/en/default.inc.php b/core/components/fileman/lexicon/en/default.inc.php
new file mode 100644
index 0000000..f730cf0
--- /dev/null
+++ b/core/components/fileman/lexicon/en/default.inc.php
@@ -0,0 +1,4 @@
+ Загрузить с диска';
+
+$_lang['fileman_window_upload_by_url_title'] = ' Загрузить по ссылке';
+$_lang['fileman_btn_upload_by_url'] = ' Загрузить по ссылке';
+$_lang['fileman_btn_upload_by_url_tooltip'] = 'Вы можете загрузить файл просто указав ссылку на него';
+$_lang['fileman_field_url'] = 'Ссылка на файл:';
+$_lang['fileman_field_title'] = 'Название';
+$_lang['fileman_url_description'] = 'Скопируйте URL изображения, вставьте его в поле и нажмите загрузить! Также вы сразу можете задать название для загружаемого файла. Для изображений доступен предварительный просмотр.';
+$_lang['fileman_url_close_window'] = 'Не закрывать окно после загрузки';
+
+$_lang['fileman_id'] = 'ID';
+$_lang['fileman_sort_order'] = 'Порядок';
+$_lang['fileman_fid'] = 'ID файла';
+$_lang['fileman_name'] = 'Название файла';
+$_lang['fileman_internal_name'] = 'Внутреннее название файла';
+$_lang['fileman_extension'] = 'Расширение файла';
+$_lang['fileman_path'] = 'Путь';
+$_lang['fileman_title'] = 'Заголовок';
+$_lang['fileman_description'] = 'Описание';
+$_lang['fileman_group'] = 'Группа';
+$_lang['fileman_hash'] = 'Контрольная сумма SHA1';
+$_lang['fileman_size'] = 'Размер';
+$_lang['fileman_download'] = 'Скачано';
+
+$_lang['fileman_file_tab_general'] = 'Файл';
+$_lang['fileman_file_tab_settings'] = 'Настройки';
+
+$_lang['fileman_file_err_name'] = 'Вы должны указать название файла.';
+$_lang['fileman_file_err_ae'] = 'файл с таким именем уже существует.';
+$_lang['fileman_file_err_nf'] = 'файл не найден.';
+$_lang['fileman_file_err_ns'] = 'файл не указан.';
+$_lang['fileman_file_err_nr'] = 'файл не переименован.';
+$_lang['fileman_file_err_remove'] = 'Ошибка при удалении файла.';
+$_lang['fileman_file_err_save'] = 'Ошибка при сохранении файла.';
+
+$_lang['fileman_reset_downloads_confirm'] = 'Вы уверены, что хотите сбросить счет загрузок?';
+$_lang['fileman_resets_downloads_confirm'] = 'Вы уверены, что хотите сбросить счет загрузок для выбранных файлов?';
+$_lang['fileman_calculate'] = 'Вычислить хэш';
+$_lang['fileman_ddtext'] = 'Потяните и отпустите';
diff --git a/core/components/fileman/lexicon/ru/permissions.inc.php b/core/components/fileman/lexicon/ru/permissions.inc.php
new file mode 100644
index 0000000..6d50c0f
--- /dev/null
+++ b/core/components/fileman/lexicon/ru/permissions.inc.php
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/components/fileman/src/FileMan.php b/core/components/fileman/src/FileMan.php
new file mode 100644
index 0000000..fd75d79
--- /dev/null
+++ b/core/components/fileman/src/FileMan.php
@@ -0,0 +1,210 @@
+modx = $modx;
+
+ if (!$this->pdoTools) {
+ $this->loadPdoTools();
+ }
+
+ $corePath = MODX_CORE_PATH . 'components/fileman/';
+ $assetsUrl = MODX_ASSETS_URL . 'components/fileman/';
+
+ $this->config = array_merge([
+ 'assetsUrl' => $assetsUrl,
+ 'cssUrl' => $assetsUrl . 'css/',
+ 'jsUrl' => $assetsUrl . 'js/',
+ 'connectorUrl' => $assetsUrl . 'connector.php',
+
+ 'corePath' => $corePath,
+ 'modelPath' => $corePath . 'model/',
+ 'processorsPath' => $corePath . 'src/Processors/',
+
+ 'chunksPath' => $corePath . 'elements/chunks/',
+ 'templatesPath' => $corePath . 'elements/templates/',
+ 'chunkSuffix' => '.chunk.tpl',
+ 'snippetsPath' => $corePath . 'elements/snippets/',
+
+ ], $config);
+
+ $this->modx->lexicon->load('fileman:default');
+
+ $this->config = array_merge($this->config, array(
+ 'file_fields' => $this->getFileFields(),
+ 'files_grid_fields' => $this->getFileGridFields(),
+ 'resource_id' => '',
+ ));
+ }
+
+ /**
+ * This method returns the list of File fields.
+ * @return array
+ * */
+ public function getFileFields()
+ {
+ return array_merge(array_keys($this->modx->getFields(File::class)), array('pagetitle', 'username'));
+ }
+
+ /**
+ * This method returns the list of fields in the message grid.
+ * @return array
+ * */
+ public function getFileGridFields()
+ {
+ $grid_fields = $this->modx->getOption('fileman_grid_fields');
+ $grid_fields = array_map('trim', explode(',', $grid_fields));
+ return array_values(array_intersect($grid_fields, $this->getFileFields()));
+ }
+
+ /**
+ * Process and return the output from a Chunk by name.
+ *
+ * @param string $name The name of the chunk.
+ * @param array $properties An associative array of properties to process the Chunk with, treated as placeholders within the scope of the Element.
+ * @param boolean $fastMode If false, all MODX tags in chunk will be processed.
+ *
+ * @return string The processed output of the Chunk.
+ */
+ public function getChunk($name, array $properties = array(), $fastMode = false) {
+ if (!$this->pdoTools) {
+ $this->loadPdoTools();
+ }
+ return $this->pdoTools->getChunk($name, $properties, $fastMode);
+ }
+
+ /**
+ * Loads an instance of pdoTools
+ *
+ * @return boolean
+ */
+ public function loadPdoTools() {
+
+ try {
+ $this->pdoTools = $this->modx->services->get('pdotools');
+ } catch (ContainerException | NotFoundException $e) {
+ $this->modx->log(modx::LOG_LEVEL_ERROR, "[FileMan] Fatal: can`t get pdoTools service (ContainerException | NotFoundException)");
+ return false;
+ }
+ return true;
+ }
+
+ public function download($fid) {
+ if(empty($fid)) {
+ return $this->modx->sendErrorPage();
+ }
+
+ /** @var File $fileObject */
+ $fileObject = $this->modx->getObject(File::class, array('fid' => $fid));
+ if (empty($fileObject)) {
+ return $this->modx->sendErrorPage();
+ }
+
+ @session_write_close();
+
+ $perform_count = true;
+
+ // If file is private then redirect else read file directly
+ if ($fileObject->get('private')) {
+
+ $meta = $fileObject->getMetaData();
+
+ // Get file info
+ $fileName = $fileObject->getFullPath();
+
+ $fileSize = $meta['size'];
+
+ $mtime = filemtime($fileName);
+
+ if (isset($_SERVER['HTTP_RANGE'])) {
+ // Get range
+ $range = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']);
+ list($start, $end) = explode('-', $range);
+
+ // Check data
+ if (empty($start)) {
+ header($_SERVER['SERVER_PROTOCOL'] . ' 416 Requested Range Not Satisfiable');
+ return;
+ } else {
+ $perform_count = false;
+ }
+
+ // Check range
+ $start = intval($start);
+ $end = intval($end);
+
+ if (($end == 0) || ($end < $start) || ($end >= $fileSize)) $end = $fileSize - 1;
+
+ $remain = $end - $start;
+
+ if ($remain == 0) {
+ header($_SERVER['SERVER_PROTOCOL'] . ' 416 Requested Range Not Satisfiable');
+ return;
+ }
+
+ header($_SERVER['SERVER_PROTOCOL'] . ' 206 Partial Content');
+ header("Content-Range: bytes $start-$end/$fileSize");
+ } else {
+ $remain = $fileSize;
+ }
+
+ // Put headers
+ header('Last-Modified: ' . gmdate('r', $mtime));
+ header('ETag: ' . sprintf('%x-%x-%x', fileinode($fileName), $fileSize, $mtime));
+ header('Accept-Ranges: bytes');
+ header('Content-Type: application/force-download');
+ header('Content-Length: ' . $remain);
+ header('Content-Disposition: attachment; filename="' . $fileObject->get('name') . '"');
+ header('Connection: close');
+
+ if ($range) {
+ $fh = fopen($fileName, 'rb');
+ fseek($fh, $start);
+
+ // Output contents
+ $blocksize = 8192;
+
+ while (!feof($fh) && ($remain > 0)) {
+ echo fread($fh, ($remain > $blocksize) ? $blocksize : $remain);
+ flush();
+
+ $remain -= $blocksize;
+ }
+
+ fclose($fh);
+ } else {
+ readfile($fileName);
+ }
+ } else {
+ // In public mode redirect to file url
+ $fileUrl = $fileObject->getUrl();
+ header("Location: $fileUrl", true, 302);
+ }
+
+ // Count downloads if allowed by config
+ if ($perform_count && $this->modx->getOption('fileman_download', null, true)) {
+
+ $count = $fileObject->get('download');
+ $fileObject->set('download', $count + 1);
+ $fileObject->save();
+ }
+ }
+}
diff --git a/core/components/fileman/src/Model/File.php b/core/components/fileman/src/Model/File.php
new file mode 100644
index 0000000..635548a
--- /dev/null
+++ b/core/components/fileman/src/Model/File.php
@@ -0,0 +1,234 @@
+source)
+ return $this->source;
+
+ //get modMediaSource
+ $mediaSourceId = $this->xpdo->getOption('fileman_mediasource', null, 1);
+
+ /** @var modMediaSource $mediaSource */
+ $mediaSource = $this->xpdo->getObject(modMediaSource::class, array('id' => $mediaSourceId));
+ $mediaSource->initialize();
+ $this->source = $mediaSource;
+
+ return $this->source;
+ }
+
+ /**
+ * Get object URL
+ *
+ * @return string
+ */
+ function getUrl()
+ {
+ $ms = $this->getMediaSource();
+ return $ms->getBaseUrl() . $this->getPath();
+ }
+
+ /**
+ * Get relative file path
+ *
+ * @return string
+ */
+ function getPath()
+ {
+ return $this->get('path') . $this->get('internal_name');
+ }
+
+ /**
+ * Get full file path in fs
+ *
+ * @return string
+ */
+ function getFullPath()
+ {
+ $ms = $this->getMediaSource();
+ return $ms->getBasePath() . $this->getPath();
+ }
+
+ /**
+ * Get file meta data
+ *
+ * @return string
+ */
+ function getMetaData()
+ {
+ $ms = $this->getMediaSource();
+ $path = $this->getPath();
+ return $ms->getMetaData($path);
+ }
+
+ /**
+ * Get file size
+ *
+ * @return string
+ */
+ function getSize()
+ {
+ $meta = $this->getMetaData();
+ return $meta['size'];
+ }
+
+ /**
+ * Rename file
+ *
+ * @param string $newName
+ * @return boolean
+ */
+ function rename($newName)
+ {
+ $ms = $this->getMediaSource();
+
+ if ($ms->renameObject($this->get('path') . $this->get('internal_name'), $newName)) {
+ $this->set('name', $newName);
+ $this->set('internal_name', $newName);
+ $this->set('extension', strtolower(pathinfo($newName, PATHINFO_EXTENSION)));
+ } else {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Set privacy mode
+ *
+ * @param boolean $private
+ * @return boolean
+ */
+ function setPrivate($private)
+ {
+ if ($this->get('private') == $private) {
+ return true;
+ }
+
+ $ms = $this->getMediaSource();
+
+ $path = $this->get('path');
+
+ $extension = pathinfo($this->get('name'), PATHINFO_EXTENSION);
+ $extension = strtolower($extension);
+
+ // Generate name and check for existence
+ $filename = $private ? $this->generateName() . "." . $extension : $this->get('name');
+
+ // Получим список имен файлов в контейнере
+ $files = [];
+ foreach($ms->getObjectsInContainer($path) as $fi) {
+ $files[] = mb_strtolower($fi['name']);
+ };
+
+ // генерируем новое имя файла, если вдруг такое уже есть в текущем контейнере
+ // TODO: потенциально бесконечный цикл, нужно исправить
+ while(in_array($filename, $files)) {
+ if ($private)
+ $filename = $this->generateName() . "." . $extension;
+ else
+ $filename = $this->generateName(4) . '_' . $filename;
+ }
+
+ if ($ms->renameObject($this->get('path') . $this->get('internal_name'), $filename)) {
+ $this->set('internal_name', $filename);
+ $this->set('private', $private);
+ $this->save();
+
+ return true;
+ } else {
+ $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, '[FileMan] An error occurred while trying to rename the attachment file at: ' . $filename);
+ }
+
+ return false;
+ }
+
+ /**
+ * Remove file and object
+ *
+ * @param array $ancestors
+ * @return bool
+ */
+ function remove(array $ancestors = array())
+ {
+ $filename = $this->getPath();
+ if (!empty($filename)) {
+ $ms = $this->getMediaSource();
+ if (!@$ms->removeObject($filename))
+ $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, '[FileMan] An error occurred while trying to remove the attachment file at: ' . $filename);
+ }
+
+ return parent::remove($ancestors);
+ }
+
+ /* Generate Filename
+ *
+ * @param integer $length Length of generated sequence
+ * @return string
+ */
+ static function generateName($length = 32)
+ {
+ $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_';
+ $charactersLength = strlen($characters);
+
+ $newName = '';
+
+ for ($i = 0; $i < $length; $i++)
+ $newName .= $characters[rand(0, $charactersLength - 1)];
+
+ return $newName;
+ }
+
+ /* Sanitize Filename
+ *
+ * @param string $str Input file name
+ * @return string
+ */
+ static function sanitizeName($str)
+ {
+ $bad = array(
+ '../', '', '<', '>',
+ "'", '"', '&', '$', '#',
+ '{', '}', '[', ']', '=',
+ ';', '?', '%20', '%22',
+ '%3c', // <
+ '%253c', // <
+ '%3e', // >
+ '%0e', // >
+ '%28', // (
+ '%29', // )
+ '%2528', // (
+ '%26', // &
+ '%24', // $
+ '%3f', // ?
+ '%3b', // ;
+ '%3d', // =
+ '/', './', '\\'
+ );
+
+ return stripslashes(str_replace($bad, '', $str));
+ }
+}
diff --git a/core/components/fileman/src/Model/metadata.mysql.php b/core/components/fileman/src/Model/metadata.mysql.php
new file mode 100644
index 0000000..619893e
--- /dev/null
+++ b/core/components/fileman/src/Model/metadata.mysql.php
@@ -0,0 +1,13 @@
+ '3.0',
+ 'namespace' => 'FileMan\\Model',
+ 'namespacePrefix' => 'FileMan',
+ 'class_map' =>
+ array (
+ 'xPDO\\Om\\xPDOSimpleObject' =>
+ array (
+ 0 => 'FileMan\\Model\\File',
+ ),
+ ),
+);
\ No newline at end of file
diff --git a/core/components/fileman/src/Model/mysql/File.php b/core/components/fileman/src/Model/mysql/File.php
new file mode 100644
index 0000000..89ab86d
--- /dev/null
+++ b/core/components/fileman/src/Model/mysql/File.php
@@ -0,0 +1,242 @@
+ 'FileMan\\Model',
+ 'version' => '3.0',
+ 'table' => 'fileman_files',
+ 'extends' => 'xPDO\\Om\\xPDOSimpleObject',
+ 'tableMeta' =>
+ array (
+ 'engine' => 'InnoDB',
+ ),
+ 'fields' =>
+ array (
+ 'fid' => '',
+ 'name' => '',
+ 'internal_name' => '',
+ 'extension' => '',
+ 'path' => '',
+ 'title' => '',
+ 'description' => '',
+ 'group' => '',
+ 'size' => 0,
+ 'hash' => '',
+ 'private' => 0,
+ 'download' => 0,
+ 'resource_id' => 0,
+ 'user_id' => 0,
+ 'sort_order' => 0,
+ ),
+ 'fieldMeta' =>
+ array (
+ 'fid' =>
+ array (
+ 'dbtype' => 'varchar',
+ 'precision' => '40',
+ 'phptype' => 'string',
+ 'null' => false,
+ 'default' => '',
+ ),
+ 'name' =>
+ array (
+ 'dbtype' => 'varchar',
+ 'precision' => '255',
+ 'phptype' => 'string',
+ 'null' => false,
+ 'default' => '',
+ ),
+ 'internal_name' =>
+ array (
+ 'dbtype' => 'varchar',
+ 'precision' => '255',
+ 'phptype' => 'string',
+ 'null' => false,
+ 'default' => '',
+ ),
+ 'extension' =>
+ array (
+ 'dbtype' => 'varchar',
+ 'precision' => '50',
+ 'phptype' => 'string',
+ 'null' => false,
+ 'default' => '',
+ ),
+ 'path' =>
+ array (
+ 'dbtype' => 'varchar',
+ 'precision' => '100',
+ 'phptype' => 'string',
+ 'null' => false,
+ 'default' => '',
+ ),
+ 'title' =>
+ array (
+ 'dbtype' => 'varchar',
+ 'precision' => '1023',
+ 'phptype' => 'string',
+ 'null' => false,
+ 'default' => '',
+ ),
+ 'description' =>
+ array (
+ 'dbtype' => 'text',
+ 'phptype' => 'string',
+ 'null' => false,
+ 'default' => '',
+ ),
+ 'group' =>
+ array (
+ 'dbtype' => 'varchar',
+ 'precision' => '255',
+ 'phptype' => 'string',
+ 'null' => false,
+ 'default' => '',
+ ),
+ 'size' =>
+ array (
+ 'dbtype' => 'int',
+ 'precision' => '10',
+ 'phptype' => 'integer',
+ 'attributes' => 'unsigned',
+ 'null' => false,
+ 'default' => 0,
+ ),
+ 'hash' =>
+ array (
+ 'dbtype' => 'varchar',
+ 'precision' => '50',
+ 'phptype' => 'string',
+ 'null' => false,
+ 'default' => '',
+ ),
+ 'private' =>
+ array (
+ 'dbtype' => 'tinyint',
+ 'precision' => '1',
+ 'phptype' => 'boolean',
+ 'attributes' => 'unsigned',
+ 'null' => false,
+ 'default' => 0,
+ ),
+ 'download' =>
+ array (
+ 'dbtype' => 'int',
+ 'precision' => '10',
+ 'phptype' => 'integer',
+ 'attributes' => 'unsigned',
+ 'null' => false,
+ 'default' => 0,
+ ),
+ 'resource_id' =>
+ array (
+ 'dbtype' => 'int',
+ 'precision' => '10',
+ 'phptype' => 'integer',
+ 'null' => false,
+ 'default' => 0,
+ ),
+ 'user_id' =>
+ array (
+ 'dbtype' => 'int',
+ 'precision' => '10',
+ 'phptype' => 'integer',
+ 'null' => false,
+ 'default' => 0,
+ ),
+ 'sort_order' =>
+ array (
+ 'dbtype' => 'int',
+ 'precision' => '10',
+ 'phptype' => 'integer',
+ 'attributes' => 'unsigned',
+ 'null' => false,
+ 'default' => 0,
+ ),
+ ),
+ 'indexes' =>
+ array (
+ 'fid' =>
+ array (
+ 'alias' => 'fid',
+ 'primary' => false,
+ 'unique' => false,
+ 'type' => 'BTREE',
+ 'columns' =>
+ array (
+ 'fid' =>
+ array (
+ 'length' => '',
+ 'collation' => 'A',
+ 'null' => false,
+ ),
+ ),
+ ),
+ 'name' =>
+ array (
+ 'alias' => 'name',
+ 'primary' => false,
+ 'unique' => false,
+ 'type' => 'BTREE',
+ 'columns' =>
+ array (
+ 'name' =>
+ array (
+ 'length' => '',
+ 'collation' => 'A',
+ 'null' => false,
+ ),
+ ),
+ ),
+ 'resource_id' =>
+ array (
+ 'alias' => 'resource_id',
+ 'primary' => false,
+ 'unique' => false,
+ 'type' => 'BTREE',
+ 'columns' =>
+ array (
+ 'resource_id' =>
+ array (
+ 'length' => '',
+ 'collation' => 'A',
+ 'null' => false,
+ ),
+ ),
+ ),
+ 'user_id' =>
+ array (
+ 'alias' => 'user_id',
+ 'primary' => false,
+ 'unique' => false,
+ 'type' => 'BTREE',
+ 'columns' =>
+ array (
+ 'user_id' =>
+ array (
+ 'length' => '',
+ 'collation' => 'A',
+ 'null' => false,
+ ),
+ ),
+ ),
+ ),
+ 'aggregates' =>
+ array (
+ 'Resource' =>
+ array (
+ 'class' => 'MODX\\Revolution\\modResource',
+ 'local' => 'resource_id',
+ 'foreign' => 'id',
+ 'cardinality' => 'one',
+ 'owner' => 'foreign',
+ ),
+ ),
+ );
+
+}
diff --git a/core/components/fileman/src/Processors/File/Access.php b/core/components/fileman/src/Processors/File/Access.php
new file mode 100644
index 0000000..07caee6
--- /dev/null
+++ b/core/components/fileman/src/Processors/File/Access.php
@@ -0,0 +1,44 @@
+checkPermissions()) {
+ return $this->failure($this->modx->lexicon('access_denied'));
+ }
+
+ $private = ($this->getProperty('private')) ? true : false;
+
+ $ids = $this->modx->fromJSON($this->getProperty('ids'));
+ if (empty($ids)) {
+ return $this->failure($this->modx->lexicon('fileman_file_err_ns'));
+ }
+
+ foreach ($ids as $id) {
+ /** @var File $object */
+ if (!$object = $this->modx->getObject($this->classKey, $id)) {
+ return $this->failure($this->modx->lexicon('fileman_file_err_nf'));
+ }
+
+ if (!$object->setPrivate($private))
+ return $this->failure($this->modx->lexicon('fileman_file_err_nf'));
+ }
+
+ return $this->success();
+ }
+}
diff --git a/core/components/fileman/src/Processors/File/Create.php b/core/components/fileman/src/Processors/File/Create.php
new file mode 100644
index 0000000..5e2d377
--- /dev/null
+++ b/core/components/fileman/src/Processors/File/Create.php
@@ -0,0 +1,42 @@
+getProperty('resource_id');
+
+ if (empty($resourceId)) {
+ $this->modx->error->addField('resource_id', $this->modx->lexicon('notset'));
+ }
+
+ $name = trim($this->getProperty('name'));
+ $name = $this->object->sanitizeName($name);
+ $this->setProperty('name', $name);
+
+ if (empty($name)) {
+ $this->modx->error->addField('name', $this->modx->lexicon('fileman_file_err_name'));
+ }
+
+ $this->setProperty('fid', $this->object->generateName());
+
+ return parent::beforeSet();
+ }
+}
diff --git a/core/components/fileman/src/Processors/File/Get.php b/core/components/fileman/src/Processors/File/Get.php
new file mode 100644
index 0000000..0178928
--- /dev/null
+++ b/core/components/fileman/src/Processors/File/Get.php
@@ -0,0 +1,30 @@
+checkPermissions()) {
+ return $this->failure($this->modx->lexicon('access_denied'));
+ }
+
+ return parent::process();
+ }
+}
diff --git a/core/components/fileman/src/Processors/File/GetList.php b/core/components/fileman/src/Processors/File/GetList.php
new file mode 100644
index 0000000..ef52f91
--- /dev/null
+++ b/core/components/fileman/src/Processors/File/GetList.php
@@ -0,0 +1,146 @@
+getProperty('resource_id'))) {
+ $this->defaultSortField = 'sort_order';
+ $this->defaultSortDirection = 'ASC';
+ }
+
+ return parent::initialize();
+ }
+
+ /**
+ * We do a special check of permissions
+ * because our objects is not an instances of modAccessibleObject
+ *
+ * @return boolean|string
+ */
+ public function beforeQuery()
+ {
+ if (!$this->checkPermissions()) {
+ return $this->modx->lexicon('access_denied');
+ }
+
+ return true;
+ }
+
+
+ /**
+ * @param xPDOQuery $c
+ *
+ * @return xPDOQuery
+ */
+ public function prepareQueryBeforeCount(xPDOQuery $c)
+ {
+ $resourceId = (int) $this->getProperty('resource_id');
+ $user = trim($this->getProperty('user'));
+ $query = trim($this->getProperty('query'));
+
+ $c->select($this->modx->getSelectColumns(File::class, 'File'));
+
+ if ($query){
+ $c->where(array(
+ 'name:LIKE' => "%{$query}%",
+ 'OR:title:LIKE' => "%{$query}%",
+ 'OR:description:LIKE' => "%{$query}%",
+ 'OR:group:LIKE' => "%{$query}%"
+ ));
+ }
+
+ if ($user || ($resourceId == 0)) {
+ $c->select('User.username');
+ $c->leftJoin(modUser::class, 'User', 'User.id=File.user_id');
+ }
+
+ if ($user)
+ $c->where(array('User.username:LIKE' => "%$user%"));
+
+ if ($resourceId > 0)
+ $c->where(array('resource_id' => $resourceId));
+ else {
+ $c->select('Resource.pagetitle');
+ $c->leftJoin('modResource', 'Resource', 'Resource.id=File.resource_id');
+ }
+
+ return $c;
+ }
+
+
+ /**
+ * @param xPDOObject $object
+ *
+ * @return array
+ */
+ /*public function prepareRow(xPDOObject $object)
+ {
+ $array = $object->toArray();
+ $array['actions'] = [];
+
+ // Edit
+ $array['actions'][] = [
+ 'cls' => '',
+ 'icon' => 'icon icon-edit',
+ 'title' => $this->modx->lexicon('fileman_item_update'),
+ //'multiple' => $this->modx->lexicon('fileman_items_update'),
+ 'action' => 'updateItem',
+ 'button' => true,
+ 'menu' => true,
+ ];
+
+ if (!$array['active']) {
+ $array['actions'][] = [
+ 'cls' => '',
+ 'icon' => 'icon icon-power-off action-green',
+ 'title' => $this->modx->lexicon('fileman_item_enable'),
+ 'multiple' => $this->modx->lexicon('fileman_items_enable'),
+ 'action' => 'enableItem',
+ 'button' => true,
+ 'menu' => true,
+ ];
+ } else {
+ $array['actions'][] = [
+ 'cls' => '',
+ 'icon' => 'icon icon-power-off action-gray',
+ 'title' => $this->modx->lexicon('fileman_item_disable'),
+ 'multiple' => $this->modx->lexicon('fileman_items_disable'),
+ 'action' => 'disableItem',
+ 'button' => true,
+ 'menu' => true,
+ ];
+ }
+
+ // Remove
+ $array['actions'][] = [
+ 'cls' => '',
+ 'icon' => 'icon icon-trash-o action-red',
+ 'title' => $this->modx->lexicon('fileman_item_remove'),
+ 'multiple' => $this->modx->lexicon('fileman_items_remove'),
+ 'action' => 'removeItem',
+ 'button' => true,
+ 'menu' => true,
+ ];
+
+ return $array;
+ }*/
+}
diff --git a/core/components/fileman/src/Processors/File/Hash.php b/core/components/fileman/src/Processors/File/Hash.php
new file mode 100644
index 0000000..fb355c3
--- /dev/null
+++ b/core/components/fileman/src/Processors/File/Hash.php
@@ -0,0 +1,38 @@
+checkPermissions()) {
+ return $this->failure($this->modx->lexicon('access_denied'));
+ }
+
+ $hash = sha1($this->object->getFullPath());
+
+ $this->object->set('hash', $hash);
+ $this->object->save();
+
+ return $this->success('', array('hash' => $hash));
+ }
+}
diff --git a/core/components/fileman/src/Processors/File/Remove.php b/core/components/fileman/src/Processors/File/Remove.php
new file mode 100644
index 0000000..1908c72
--- /dev/null
+++ b/core/components/fileman/src/Processors/File/Remove.php
@@ -0,0 +1,41 @@
+checkPermissions()) {
+ return $this->failure($this->modx->lexicon('access_denied'));
+ }
+
+ $ids = json_decode($this->getProperty('ids'), true);
+ if (empty($ids)) {
+ return $this->failure($this->modx->lexicon('fileman_file_err_ns'));
+ }
+
+ foreach ($ids as $id) {
+ /** @var File $object */
+ if (!$object = $this->modx->getObject($this->classKey, $id)) {
+ return $this->failure($this->modx->lexicon('fileman_file_err_nf'));
+ }
+
+ $object->remove();
+ }
+
+ return $this->success();
+ }
+}
diff --git a/core/components/fileman/src/Processors/File/Reset.php b/core/components/fileman/src/Processors/File/Reset.php
new file mode 100644
index 0000000..2184199
--- /dev/null
+++ b/core/components/fileman/src/Processors/File/Reset.php
@@ -0,0 +1,36 @@
+modx->fromJSON($this->getProperty('ids'));
+
+ if (empty($ids))
+ return $this->failure($this->modx->lexicon('fileman_item_err_ns'));
+
+ foreach ($ids as $id) {
+ /** @var File $object */
+ if (!$object = $this->modx->getObject($this->classKey, $id))
+ return $this->failure($this->modx->lexicon('fileman_item_err_nf'));
+
+ $object->set('download', 0);
+ $object->save();
+ }
+
+ return $this->success();
+ }
+
+}
diff --git a/core/components/fileman/src/Processors/File/Sort.php b/core/components/fileman/src/Processors/File/Sort.php
new file mode 100644
index 0000000..d37383e
--- /dev/null
+++ b/core/components/fileman/src/Processors/File/Sort.php
@@ -0,0 +1,36 @@
+modx->fromJSON($this->getProperty('sort_order'));
+
+ if (empty($order))
+ return $this->failure($this->modx->lexicon('fileman_item_err_ns'));
+
+ foreach ($order as $id => $value) {
+ /** @var File $object */
+ if (!$object = $this->modx->getObject($this->classKey, $id))
+ return $this->failure($this->modx->lexicon('fileman_item_err_nf'));
+
+ $object->set('sort_order', $value);
+ $object->save();
+ }
+
+ return $this->success();
+ }
+}
diff --git a/core/components/fileman/src/Processors/File/Update.php b/core/components/fileman/src/Processors/File/Update.php
new file mode 100644
index 0000000..de750a8
--- /dev/null
+++ b/core/components/fileman/src/Processors/File/Update.php
@@ -0,0 +1,73 @@
+checkPermissions()) {
+ return $this->modx->lexicon('access_denied');
+ }
+
+ return true;
+ }
+
+
+ /**
+ * @return bool
+ */
+ public function beforeSet()
+ {
+
+ $resourceId = (int)$this->getProperty('resource_id');
+
+ if (!$resourceId) {
+ $this->modx->error->addField('resource_id', $this->modx->lexicon('notset'));
+ }
+
+ $private = ($this->getProperty('private')) ? true : false;
+
+ // Allow filename change only in private mode. May be changed further
+ $name = trim($this->getProperty('name'));
+ $name = $this->object->sanitizeName($name);
+ if (empty($name)) {
+ $this->modx->error->addField('name', $this->modx->lexicon('fileman_file_err_name'));
+ }
+
+ // If file is open we should rename file, otherwize just change field value
+ if (!$this->object->get('private')) {
+ $this->unsetProperty('name');
+
+ // Rename if name changed
+ if ($name != $this->object->get('name')) {
+ if (!$this->object->rename($name)) {
+ $this->modx->error->addField('name', $this->modx->lexicon('fileman_file_err_nr'));
+ }
+ }
+ }
+
+ if (!$this->object->setPrivate($private)) {
+ $this->modx->error->addField('name', $this->modx->lexicon('fileman_file_err_nr'));
+ }
+
+ return parent::beforeSet();
+ }
+}
diff --git a/core/components/fileman/src/Processors/File/Upload.php b/core/components/fileman/src/Processors/File/Upload.php
new file mode 100644
index 0000000..2b73778
--- /dev/null
+++ b/core/components/fileman/src/Processors/File/Upload.php
@@ -0,0 +1,449 @@
+modx->hasPermission($this->permission);
+ }
+
+ public function getLanguageTopics()
+ {
+ return $this->languageTopics;
+ }
+
+ public function initialize()
+ {
+ $this->setDefaultProperties(array('resource_id' => 0));
+
+ $this->uploadPath = $this->preparePath($this->modx->getOption('fileman_path'));
+ $this->privateMode = $this->modx->getOption('fileman_private');
+ $this->autoTitle = $this->modx->getOption('fileman_auto_title', null, true);
+ $this->calcHash = $this->modx->getOption('fileman_calchash');
+
+ $this->setProperty('source', $this->modx->getOption('fileman_mediasource', null, 1));
+
+ $this->mediaSource = $this->initializeMediaSource($this->getProperty('source'));
+
+ if (!$this->mediaSource) {
+ $this->modx->error->addError($this->modx->lexicon('permission_denied'));
+ return false;
+ }
+ if (!$this->uploadPath) {
+ $this->modx->error->addError($this->modx->lexicon('file_folder_err_ns'));
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Получает и инициализирует Media source
+ * @param $source integer Media source id
+ * @return modMediaSource|boolean
+ */
+ private function initializeMediaSource($mediaSourceId)
+ {
+ /** @var modMediaSource $mediaSource */
+ $mediaSource = $this->modx->getObject(modMediaSource::class, array('id' => $mediaSourceId));
+ $mediaSource->initialize();
+
+ if (empty($mediaSource) || !$mediaSource->getWorkingContext())
+ return false;
+
+ $mediaSource->setRequestProperties($this->getProperties());
+ $mediaSource->initialize();
+
+ return $mediaSource;
+ }
+
+
+
+ /**
+ * @param $internalPath
+ * @return bool|string
+ */
+ private function mediaSourceCreateContainer($containerName)
+ {
+ if ($containerName !== "/") {
+ if (!$this->mediaSource->createContainer($containerName, '')) {
+ $this->modx->log(modX::LOG_LEVEL_ERROR, '[FileMan] Can`t create container: ' . $containerName);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private function saveTmpFile($url)
+ {
+ // TODO: добавить обработку ошибок сохранения файла
+ //$tmp_dir = MODX_ASSETS_PATH.'components/fileman/tmp/';
+ $tmp_dir = ini_get('upload_tmp_dir') ? ini_get('upload_tmp_dir') : sys_get_temp_dir();
+ $tmpFile = tempnam($tmp_dir, "file_man_");
+ file_put_contents($tmpFile, file_get_contents($url));
+
+ $fn = parse_url($url, PHP_URL_PATH);
+ $fn = pathinfo($fn, PATHINFO_BASENAME);
+
+ $title = $this->getProperty("title");
+
+ return array(
+ 'name' => $fn,
+ 'type' => mime_content_type($tmpFile),
+ 'temp_file_name' => $tmpFile,
+ 'flag_remove_temp_file' => true,
+ 'error' => '0',
+ 'size' => filesize($tmpFile),
+ 'title' => $title
+ );
+ }
+
+
+ public function process()
+ {
+ if (!$this->mediaSource->checkPolicy('create')) {
+ return $this->failure($this->modx->lexicon('permission_denied'));
+ }
+
+ // Создадим контейнер (папку) в источнике
+ if ($this->mediaSourceCreateContainer($this->uploadPath) === false) {
+ return $this->failure($this->modx->lexicon('fileman_file_err_save'));
+ };
+
+ // Массив файлов, которые будем загружать
+ $files = [];
+ if ($url = $this->getProperty("url")) {
+ // Сценарий №1: Загрузка по url, создадим массив с одним элементом
+ $files[] = $this->saveTmpFile($url);
+ } else {
+ // Сценарий №2: мы работаем с POST запросом, содержащим $_FILES
+ $files = $_FILES;
+ }
+
+ $result = array();
+ foreach ($files as $file) {
+ $nameWithoutExtension = pathinfo($file['name'], PATHINFO_FILENAME);
+ $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
+ if(empty($extension)) {
+ $extension = $this->mime2ext($file['type']);
+ }
+
+ // Имя файла, для privateMode оно будет случайным
+ $internalName = $this->privateMode
+ ? File::generateName() . "." . $extension
+ : $nameWithoutExtension . "." . $extension;
+
+ // Если установлена настройка upload_translit, то имя файла может измениться, учтем это
+ if ((boolean)$this->modx->getOption('upload_translit')) {
+ $internalName = $this->modx->filterPathSegment($internalName);
+ $internalName = $this->mediaSource->sanitizePath($internalName);
+ }
+
+ // Загружаем файл в источник
+ $uploadResult = $this->mediaSource->uploadObjectsToContainer(
+ $this->uploadPath,
+ array(
+ array_merge($file, array('name' => $internalName))
+ )
+ );
+
+ // Удаляем временный файл после успешной загрузки, если его ранее создавали
+ if (isset($file['flag_remove_temp_file'])) {
+ unlink($file['temp_file_name']);
+ }
+
+ // Обработка ошибок
+ if (!$uploadResult) {
+ $msg = '';
+ $errors = $this->mediaSource->getErrors();
+ foreach ($errors as $k => $msg) {
+ $this->modx->error->addField($k, $msg);
+ }
+ // Вернем текст последней ошибки
+ return $this->failure($msg);
+ } else {
+ // Создадим запись в БД
+ $fid = File::generateName();
+ $resourceId = $this->getProperty('resource_id');
+
+ $title = isset($file['title']) ? $file['title'] : '';
+ if ($this->autoTitle && empty($title)) {
+ $title = pathinfo($file['name'], PATHINFO_FILENAME);
+ }
+
+ $fileObject = $this->modx->newObject(File::class, array(
+ 'fid' => $fid,
+ 'resource_id' => $resourceId,
+ 'name' => $nameWithoutExtension . '.' . $extension,
+ 'internal_name' => $internalName,
+ 'extension' => $extension,
+ 'path' => $this->uploadPath,
+ 'private' => $this->privateMode,
+ 'user_id' => $this->modx->user->get('id'),
+ 'sort_order' => $this->getNextSortOrder($resourceId),
+ 'hash' => ($this->calcHash) ? sha1_file($this->uploadPath . $internalName) : ''
+ ));
+
+ if (!$fileObject->save())
+ return $this->failure($this->modx->lexicon('fileman_item_err_save'));
+
+ $result[] = $fileObject->toArray();
+ }
+ }
+
+ return $this->outputArray($result, count($result));
+ }
+
+
+ /**
+ * Get the next sort order for the files of the specified resource
+ *
+ * @param int $resourceId Id ресурса
+ * @return int
+ */
+ private function getNextSortOrder($resourceId)
+ {
+ $tableName = $this->modx->getTableName(File::class);
+ $stmt = $this->modx->query("SELECT MAX(`sort_order`) FROM {$tableName} WHERE `resource_id` = {$resourceId}");
+ $sortOrder = (int) $stmt->fetch(PDO::FETCH_COLUMN);
+ $stmt->closeCursor();
+
+ return $sortOrder + 1;
+ }
+
+ /**
+ * Substitutes the variables year, month, day, user, resource into $path
+ *
+ * @param string $path
+ * @return string
+ */
+ private function preparePath($path)
+ {
+ $search = array('{year}', '{month}', '{day}', '{user}', '{resource}');
+ $replace = array(date('Y'), date('m'), date('d'), $this->modx->user->get('id'), $this->getProperty('resource_id'));
+
+ return str_replace($search, $replace, $path);
+ }
+
+ /**
+ * Get extension from mime type
+ *
+ * @param string $mime
+ * @return string
+ */
+ function mime2ext($mime) {
+ $mime = trim($mime);
+ if(empty($mime)) {
+ return '';
+ }
+ $mime_map = [
+ 'video/3gpp2' => '3g2',
+ 'video/3gp' => '3gp',
+ 'video/3gpp' => '3gp',
+ 'application/x-compressed' => '7zip',
+ 'audio/x-acc' => 'aac',
+ 'audio/ac3' => 'ac3',
+ 'application/postscript' => 'ai',
+ 'audio/x-aiff' => 'aif',
+ 'audio/aiff' => 'aif',
+ 'audio/x-au' => 'au',
+ 'video/x-msvideo' => 'avi',
+ 'video/msvideo' => 'avi',
+ 'video/avi' => 'avi',
+ 'application/x-troff-msvideo' => 'avi',
+ 'application/macbinary' => 'bin',
+ 'application/mac-binary' => 'bin',
+ 'application/x-binary' => 'bin',
+ 'application/x-macbinary' => 'bin',
+ 'image/bmp' => 'bmp',
+ 'image/x-bmp' => 'bmp',
+ 'image/x-bitmap' => 'bmp',
+ 'image/x-xbitmap' => 'bmp',
+ 'image/x-win-bitmap' => 'bmp',
+ 'image/x-windows-bmp' => 'bmp',
+ 'image/ms-bmp' => 'bmp',
+ 'image/x-ms-bmp' => 'bmp',
+ 'application/bmp' => 'bmp',
+ 'application/x-bmp' => 'bmp',
+ 'application/x-win-bitmap' => 'bmp',
+ 'application/cdr' => 'cdr',
+ 'application/coreldraw' => 'cdr',
+ 'application/x-cdr' => 'cdr',
+ 'application/x-coreldraw' => 'cdr',
+ 'image/cdr' => 'cdr',
+ 'image/x-cdr' => 'cdr',
+ 'zz-application/zz-winassoc-cdr' => 'cdr',
+ 'application/mac-compactpro' => 'cpt',
+ 'application/pkix-crl' => 'crl',
+ 'application/pkcs-crl' => 'crl',
+ 'application/x-x509-ca-cert' => 'crt',
+ 'application/pkix-cert' => 'crt',
+ 'text/css' => 'css',
+ 'text/x-comma-separated-values' => 'csv',
+ 'text/comma-separated-values' => 'csv',
+ 'application/vnd.msexcel' => 'csv',
+ 'application/x-director' => 'dcr',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
+ 'application/x-dvi' => 'dvi',
+ 'message/rfc822' => 'eml',
+ 'application/x-msdownload' => 'exe',
+ 'video/x-f4v' => 'f4v',
+ 'audio/x-flac' => 'flac',
+ 'video/x-flv' => 'flv',
+ 'image/gif' => 'gif',
+ 'application/gpg-keys' => 'gpg',
+ 'application/x-gtar' => 'gtar',
+ 'application/x-gzip' => 'gzip',
+ 'application/mac-binhex40' => 'hqx',
+ 'application/mac-binhex' => 'hqx',
+ 'application/x-binhex40' => 'hqx',
+ 'application/x-mac-binhex40' => 'hqx',
+ 'text/html' => 'html',
+ 'image/x-icon' => 'ico',
+ 'image/x-ico' => 'ico',
+ 'image/vnd.microsoft.icon' => 'ico',
+ 'text/calendar' => 'ics',
+ 'application/java-archive' => 'jar',
+ 'application/x-java-application' => 'jar',
+ 'application/x-jar' => 'jar',
+ 'image/jp2' => 'jp2',
+ 'video/mj2' => 'jp2',
+ 'image/jpx' => 'jp2',
+ 'image/jpm' => 'jp2',
+ 'image/jpeg' => 'jpeg',
+ 'image/pjpeg' => 'jpeg',
+ 'application/x-javascript' => 'js',
+ 'application/json' => 'json',
+ 'text/json' => 'json',
+ 'application/vnd.google-earth.kml+xml' => 'kml',
+ 'application/vnd.google-earth.kmz' => 'kmz',
+ 'text/x-log' => 'log',
+ 'audio/x-m4a' => 'm4a',
+ 'application/vnd.mpegurl' => 'm4u',
+ 'audio/midi' => 'mid',
+ 'application/vnd.mif' => 'mif',
+ 'video/quicktime' => 'mov',
+ 'video/x-sgi-movie' => 'movie',
+ 'audio/mpeg' => 'mp3',
+ 'audio/mpg' => 'mp3',
+ 'audio/mpeg3' => 'mp3',
+ 'audio/mp3' => 'mp3',
+ 'video/mp4' => 'mp4',
+ 'video/mpeg' => 'mpeg',
+ 'application/oda' => 'oda',
+ 'application/vnd.oasis.opendocument.text' => 'odt',
+ 'application/vnd.oasis.opendocument.spreadsheet' => 'ods',
+ 'application/vnd.oasis.opendocument.presentation' => 'odp',
+ 'audio/ogg' => 'ogg',
+ 'video/ogg' => 'ogg',
+ 'application/ogg' => 'ogg',
+ 'application/x-pkcs10' => 'p10',
+ 'application/pkcs10' => 'p10',
+ 'application/x-pkcs12' => 'p12',
+ 'application/x-pkcs7-signature' => 'p7a',
+ 'application/pkcs7-mime' => 'p7c',
+ 'application/x-pkcs7-mime' => 'p7c',
+ 'application/x-pkcs7-certreqresp' => 'p7r',
+ 'application/pkcs7-signature' => 'p7s',
+ 'application/pdf' => 'pdf',
+ 'application/octet-stream' => 'pdf',
+ 'application/x-x509-user-cert' => 'pem',
+ 'application/x-pem-file' => 'pem',
+ 'application/pgp' => 'pgp',
+ 'application/x-httpd-php' => 'php',
+ 'application/php' => 'php',
+ 'application/x-php' => 'php',
+ 'text/php' => 'php',
+ 'text/x-php' => 'php',
+ 'application/x-httpd-php-source' => 'php',
+ 'image/png' => 'png',
+ 'image/x-png' => 'png',
+ 'application/powerpoint' => 'ppt',
+ 'application/vnd.ms-powerpoint' => 'ppt',
+ 'application/vnd.ms-office' => 'ppt',
+ 'application/msword' => 'doc',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
+ 'application/x-photoshop' => 'psd',
+ 'image/vnd.adobe.photoshop' => 'psd',
+ 'audio/x-realaudio' => 'ra',
+ 'audio/x-pn-realaudio' => 'ram',
+ 'application/x-rar' => 'rar',
+ 'application/rar' => 'rar',
+ 'application/x-rar-compressed' => 'rar',
+ 'audio/x-pn-realaudio-plugin' => 'rpm',
+ 'application/x-pkcs7' => 'rsa',
+ 'text/rtf' => 'rtf',
+ 'text/richtext' => 'rtx',
+ 'video/vnd.rn-realvideo' => 'rv',
+ 'application/x-stuffit' => 'sit',
+ 'application/smil' => 'smil',
+ 'text/srt' => 'srt',
+ 'image/svg+xml' => 'svg',
+ 'application/x-shockwave-flash' => 'swf',
+ 'application/x-tar' => 'tar',
+ 'application/x-gzip-compressed' => 'tgz',
+ 'image/tiff' => 'tiff',
+ 'text/plain' => 'txt',
+ 'text/x-vcard' => 'vcf',
+ 'application/videolan' => 'vlc',
+ 'text/vtt' => 'vtt',
+ 'audio/x-wav' => 'wav',
+ 'audio/wave' => 'wav',
+ 'audio/wav' => 'wav',
+ 'application/wbxml' => 'wbxml',
+ 'video/webm' => 'webm',
+ 'audio/x-ms-wma' => 'wma',
+ 'application/wmlc' => 'wmlc',
+ 'video/x-ms-wmv' => 'wmv',
+ 'video/x-ms-asf' => 'wmv',
+ 'application/xhtml+xml' => 'xhtml',
+ 'application/excel' => 'xl',
+ 'application/msexcel' => 'xls',
+ 'application/x-msexcel' => 'xls',
+ 'application/x-ms-excel' => 'xls',
+ 'application/x-excel' => 'xls',
+ 'application/x-dos_ms_excel' => 'xls',
+ 'application/xls' => 'xls',
+ 'application/x-xls' => 'xls',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
+ 'application/vnd.ms-excel' => 'xlsx',
+ 'application/xml' => 'xml',
+ 'text/xml' => 'xml',
+ 'text/xsl' => 'xsl',
+ 'application/xspf+xml' => 'xspf',
+ 'application/x-compress' => 'z',
+ 'application/x-zip' => 'zip',
+ 'application/zip' => 'zip',
+ 'application/x-zip-compressed' => 'zip',
+ 'application/s-compressed' => 'zip',
+ 'multipart/x-zip' => 'zip',
+ 'text/x-scriptzsh' => 'zsh',
+ ];
+
+ return isset($mime_map[$mime]) === true ? $mime_map[$mime] : '';
+ }
+}
diff --git a/core/components/fileman/src/Processors/Resource/Combo.php b/core/components/fileman/src/Processors/Resource/Combo.php
new file mode 100644
index 0000000..04099d0
--- /dev/null
+++ b/core/components/fileman/src/Processors/Resource/Combo.php
@@ -0,0 +1,95 @@
+contextKeys = $this->getContextKeys();
+ if (empty($this->contextKeys))
+ return $this->modx->lexicon('permission_denied');
+
+ return true;
+ }
+
+ /**
+ * Get a collection of Context keys that the User can access for all the Resources
+ * @return array
+ */
+ public function getContextKeys()
+ {
+ $contextKeys = array();
+ $contexts = $this->modx->getCollection(modContext::class, array('key:!=' => 'mgr'));
+
+ /** @var modContext $context */
+ foreach ($contexts as $context) {
+ if ($context->checkPolicy('list'))
+ $contextKeys[] = $context->get('key');
+ }
+
+ return $contextKeys;
+ }
+
+ public function beforeIteration(array $list) {
+ $this->charset = $this->modx->getOption('modx_charset',null,'UTF-8');
+ return $list;
+ }
+
+ public function prepareQueryBeforeCount(xPDOQuery $c)
+ {
+ $id = $this->getProperty('id');
+ $query = $this->getProperty('query');
+ $templates = $this->modx->getOption('fileman_templates');
+
+ $where = array('context_key:IN' => $this->contextKeys);
+
+ if ($templates != '')
+ $where['template:IN'] = explode(',', $templates);
+
+ if (!empty($id)) {
+ $where['id'] = $id;
+ }
+ if (!empty($query)) {
+ $where['pagetitle:LIKE'] = "%$query%";
+ }
+
+ $c->select('id,pagetitle');
+ $c->where($where);
+
+ return $c;
+ }
+
+
+ public function prepareRow(xPDOObject $object)
+ {
+ $objectArray = $object->toArray();
+
+ $objectArray['pagetitle'] = htmlentities($objectArray['pagetitle'], ENT_COMPAT, $this->charset);
+ $objectArray['description'] = htmlentities($objectArray['description'], ENT_COMPAT, $this->charset);
+
+ return array(
+ 'id' => $objectArray['id'],
+ 'pagetitle' => $objectArray['pagetitle'],
+ 'description' => $objectArray['description'],
+ );
+ }
+}