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'], + ); + } +}