├── tests ├── codeception │ ├── _data │ │ ├── test.txt │ │ ├── version │ │ │ └── test.txt │ │ └── test.zip │ ├── _output │ │ └── .gitignore │ ├── _support │ │ ├── _generated │ │ │ └── .gitignore │ │ ├── UnitTester.php │ │ ├── FunctionalTester.php │ │ └── AcceptanceTester.php │ ├── config │ │ ├── api.php │ │ ├── unit.php │ │ └── functional.php │ ├── unit │ │ ├── _bootstrap.php │ │ ├── FolderTest.php │ │ └── MigrateFromOldTest.php │ ├── api │ │ ├── _bootstrap.php │ │ ├── FileCest.php │ │ ├── ManageCest.php │ │ └── FolderCest.php │ ├── acceptance │ │ ├── _bootstrap.php │ │ ├── GuestAccessCest.php │ │ ├── ImportZipCest.php │ │ ├── VersionCest.php │ │ ├── FileContextCest.php │ │ ├── UploadAndMoveCest.php │ │ ├── ItemSelectionCest.php │ │ ├── FolderContextCest.php │ │ └── VisibilityCest.php │ ├── functional │ │ └── _bootstrap.php │ ├── unit.suite.yml │ ├── api.suite.yml │ ├── fixtures │ │ ├── FolderFixtrue.php │ │ ├── FilesFixture.php │ │ └── data │ │ │ ├── file.php │ │ │ └── folder.php │ ├── functional.suite.yml │ ├── acceptance.suite.yml │ └── _bootstrap.php ├── config │ ├── api.php │ ├── unit.php │ ├── common.php │ ├── functional.php │ └── test.php └── codeception.yml ├── resources ├── module_image.png ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png ├── screenshot4.png ├── screenshot5.png ├── screenshot6.png ├── screenshot7.png ├── screenshot8.png ├── directorylist │ ├── drive.png │ ├── folder.png │ ├── directory.png │ ├── page_white.png │ ├── directory_join.png │ ├── page_white_php.png │ ├── directory_exand.png │ ├── directory_hassub.png │ ├── page_white_code.png │ ├── page_white_text.png │ ├── page_white_picture.png │ ├── directory_join_hassub.png │ ├── directory_last-child.png │ ├── page_white_compressed.png │ ├── directory_join_hassub_expand.png │ ├── directory_last-child_hassub.png │ └── directory_last-child_hassub_expand.png └── css │ ├── directorylist.css │ └── cfiles.css ├── docs ├── swagger │ └── build.sh └── README.md ├── .github └── workflows │ ├── php-cs-fixer.yml │ ├── rector-auto-pr.yaml.yml │ ├── marketplace-upload.yml │ ├── codeception-develop.yml │ ├── codeception-master.yml │ ├── codeception-next.yml │ └── codeception-min-version.yml ├── .gitignore ├── widgets ├── views │ ├── fileListContextMenu.php │ ├── wallEntryFolder.php │ ├── folderView.php │ ├── versionsView.php │ ├── breadcrumbBar.php │ ├── wallEntryFile.php │ ├── versionItem.php │ ├── fileSelectionMenu.php │ ├── fileListMenu.php │ └── fileList.php ├── DirectoryTree.php ├── BreadcrumbBar.php ├── DropdownButton.php ├── FileListMenu.php ├── FileSelectionMenu.php ├── FolderView.php ├── WallEntryFolder.php ├── FileSystemItem.php ├── VersionItem.php ├── WallEntryFile.php └── VersionsView.php ├── migrations ├── uninstall.php ├── m160817_180831_add_type.php ├── m201030_071320_add_download_counter.php ├── m160824_120822_add_has_wall_entry.php ├── m170211_110702_show_in_stream.php ├── m160921_1102234_remove_has_wall_entry.php ├── m160810_174011_add_description.php ├── m150720_174011_initial.php ├── m170830_122433_set_root_partent_null.php ├── m170210_154141_folderNoStream.php └── m170830_122439_foreignkeys.php ├── controllers ├── rest │ ├── CfilesController.php │ ├── FileController.php │ └── FolderController.php ├── ConfigController.php ├── ConfigContainerController.php ├── DownloadController.php ├── DeleteController.php ├── ZipController.php ├── BrowseController.php ├── UploadController.php └── VersionController.php ├── views ├── move │ ├── errors.php │ ├── directory_tree_item.php │ ├── directory_tree.php │ └── modal_move.php ├── version │ └── index.php ├── config-container │ └── index.php ├── config │ └── index.php ├── edit │ ├── modal_edit_folder.php │ └── modal_edit_file.php └── browse │ └── index.php ├── composer.json ├── extensions └── custom_pages │ └── elements │ ├── FilesElementVariable.php │ ├── FoldersElementVariable.php │ ├── FilesElement.php │ ├── FoldersElement.php │ ├── FolderElement.php │ ├── FolderElementVariable.php │ ├── FileElementVariable.php │ └── FileElement.php ├── assets └── Assets.php ├── module.json ├── models ├── ZipImportHandler.php ├── ItemInterface.php ├── forms │ ├── SelectionForm.php │ ├── VersionForm.php │ └── MoveForm.php ├── rows │ ├── FolderRow.php │ ├── SpecialFolderRow.php │ └── FileRow.php ├── ConfigureContainerForm.php └── ConfigureForm.php ├── permissions ├── ManageFiles.php └── WriteAccess.php ├── actions ├── UploadZipAction.php └── UploadAction.php ├── config.php ├── helpers └── RestDefinitions.php ├── libs ├── ZipUtil.php ├── ZipExtractor.php └── ZIPCreator.php ├── components └── UrlRule.php └── messages └── en-GB └── base.php /tests/codeception/_data/test.txt: -------------------------------------------------------------------------------- 1 | First version -------------------------------------------------------------------------------- /tests/codeception/_data/version/test.txt: -------------------------------------------------------------------------------- 1 | Second version -------------------------------------------------------------------------------- /resources/module_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/module_image.png -------------------------------------------------------------------------------- /resources/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/screenshot1.png -------------------------------------------------------------------------------- /resources/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/screenshot2.png -------------------------------------------------------------------------------- /resources/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/screenshot3.png -------------------------------------------------------------------------------- /resources/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/screenshot4.png -------------------------------------------------------------------------------- /resources/screenshot5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/screenshot5.png -------------------------------------------------------------------------------- /resources/screenshot6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/screenshot6.png -------------------------------------------------------------------------------- /resources/screenshot7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/screenshot7.png -------------------------------------------------------------------------------- /resources/screenshot8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/screenshot8.png -------------------------------------------------------------------------------- /tests/codeception/_data/test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/tests/codeception/_data/test.zip -------------------------------------------------------------------------------- /resources/directorylist/drive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/directorylist/drive.png -------------------------------------------------------------------------------- /resources/directorylist/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/directorylist/folder.png -------------------------------------------------------------------------------- /tests/codeception/_output/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /resources/directorylist/directory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/directorylist/directory.png -------------------------------------------------------------------------------- /resources/directorylist/page_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/directorylist/page_white.png -------------------------------------------------------------------------------- /resources/directorylist/directory_join.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/directorylist/directory_join.png -------------------------------------------------------------------------------- /resources/directorylist/page_white_php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/directorylist/page_white_php.png -------------------------------------------------------------------------------- /tests/codeception/_support/_generated/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /resources/directorylist/directory_exand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/directorylist/directory_exand.png -------------------------------------------------------------------------------- /resources/directorylist/directory_hassub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/directorylist/directory_hassub.png -------------------------------------------------------------------------------- /resources/directorylist/page_white_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/directorylist/page_white_code.png -------------------------------------------------------------------------------- /resources/directorylist/page_white_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/cfiles/HEAD/resources/directorylist/page_white_text.png -------------------------------------------------------------------------------- /tests/codeception/config/api.php: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 14 |
-------------------------------------------------------------------------------- /migrations/uninstall.php: -------------------------------------------------------------------------------- 1 | dropTable('cfiles_file'); 10 | $this->dropTable('cfiles_folder'); 11 | } 12 | 13 | public function down() 14 | { 15 | echo "uninstall does not support migration down.\n"; 16 | return false; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/config/test.php: -------------------------------------------------------------------------------- 1 | ['cfiles'], 12 | 'fixtures' => [ 13 | 'default', 14 | 'files' => \humhub\modules\cfiles\tests\codeception\fixtures\FilesFixture::class, 15 | ], 16 | ]; 17 | -------------------------------------------------------------------------------- /tests/codeception/unit.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | 3 | # suite for unit (internal) tests. 4 | # RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. 5 | 6 | actor: UnitTester 7 | modules: 8 | enabled: 9 | - tests\codeception\_support\CodeHelper 10 | - Yii2 11 | config: 12 | Yii2: 13 | configFile: 'codeception/config/unit.php' 14 | transaction: false 15 | -------------------------------------------------------------------------------- /controllers/rest/CfilesController.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | title), $folderUrl); ?>
11 |
12 | 13 | 'btn btn-sm btn-light', 'data-ui-loader' => '']); ?> 14 | 15 |
16 | -------------------------------------------------------------------------------- /tests/codeception/api.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | 3 | # suite for REST API tests. 4 | # RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. 5 | 6 | actor: ApiTester 7 | modules: 8 | enabled: 9 | - REST 10 | - Yii2 11 | - tests\codeception\_support\DynamicFixtureHelper 12 | config: 13 | REST: 14 | url: 'http://localhost:8080/api/v1/' 15 | depends: Yii2 16 | part: Json 17 | Yii2: 18 | configFile: 'codeception/config/api.php' 19 | -------------------------------------------------------------------------------- /tests/codeception/fixtures/FolderFixtrue.php: -------------------------------------------------------------------------------- 1 | 13 | hasErrors()) : ?> 14 |
15 | 20 |
21 | 22 | -------------------------------------------------------------------------------- /migrations/m160817_180831_add_type.php: -------------------------------------------------------------------------------- 1 | addColumn('cfiles_folder', 'type', $this->string(32)); 11 | } 12 | 13 | public function down() 14 | { 15 | echo "m160817_180831_add_type cannot be reverted.\n"; 16 | 17 | return false; 18 | } 19 | 20 | /* 21 | * // Use safeUp/safeDown to run migration code within a transaction public function safeUp() { } public function safeDown() { } 22 | */ 23 | } 24 | -------------------------------------------------------------------------------- /tests/codeception/fixtures/FilesFixture.php: -------------------------------------------------------------------------------- 1 | addColumn('cfiles_file', 'download_count', $this->integer()->unsigned()->defaultValue(0)->notNull()); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function safeDown() 22 | { 23 | $this->dropColumn('cfiles_file', 'download_count'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/codeception/functional.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | 3 | # suite for functional (integration) tests. 4 | # emulate web requests and make application process them. 5 | # (tip: better to use with frameworks). 6 | 7 | # RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. 8 | actor: FunctionalTester 9 | modules: 10 | enabled: 11 | - Filesystem 12 | - Yii2 13 | - tests\codeception\_support\TestHelper 14 | - tests\codeception\_support\DynamicFixtureHelper 15 | - tests\codeception\_support\HumHubHelper 16 | config: 17 | Yii2: 18 | configFile: 'codeception/config/functional.php' 19 | -------------------------------------------------------------------------------- /migrations/m160824_120822_add_has_wall_entry.php: -------------------------------------------------------------------------------- 1 | addColumn('cfiles_folder', 'has_wall_entry', $this->boolean()->defaultValue(false)); 11 | } 12 | 13 | public function down() 14 | { 15 | echo "m160824_120822_add_has_wall_entry cannot be reverted.\n"; 16 | 17 | return false; 18 | } 19 | 20 | /* 21 | * // Use safeUp/safeDown to run migration code within a transaction public function safeUp() { } public function safeDown() { } 22 | */ 23 | } 24 | -------------------------------------------------------------------------------- /widgets/DirectoryTree.php: -------------------------------------------------------------------------------- 1 | update('file', ['show_in_stream' => false], ['object_model' => \humhub\modules\cfiles\models\File::class]); 10 | } 11 | 12 | public function down() 13 | { 14 | echo "m170211_110702_show_in_stream cannot be reverted.\n"; 15 | 16 | return false; 17 | } 18 | 19 | /* 20 | // Use safeUp/safeDown to run migration code within a transaction 21 | public function safeUp() 22 | { 23 | } 24 | 25 | public function safeDown() 26 | { 27 | } 28 | */ 29 | } 30 | -------------------------------------------------------------------------------- /views/version/index.php: -------------------------------------------------------------------------------- 1 | 15 | 16 | Yii::t('CfilesModule.base', 'File versions'), 18 | 'footer' => ModalButton::cancel(Yii::t('CfilesModule.base', 'Close')), 19 | ]) ?> 20 | 21 | $model->file]) ?> 22 | 23 | 24 | -------------------------------------------------------------------------------- /views/move/directory_tree_item.php: -------------------------------------------------------------------------------- 1 | 14 | 15 |
  • 16 | title ?> 17 | 18 | 23 | 24 |
  • 25 | -------------------------------------------------------------------------------- /extensions/custom_pages/elements/FilesElementVariable.php: -------------------------------------------------------------------------------- 1 | getItems() as $file) { 20 | $this->items[] = FileElementVariable::instance($elementContent)->setRecord($file); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /migrations/m160921_1102234_remove_has_wall_entry.php: -------------------------------------------------------------------------------- 1 | dropColumn('cfiles_folder', 'has_wall_entry'); 12 | } catch (Exception) { 13 | Yii::error("Could not drop haswall entry column", 'cfiles'); 14 | } 15 | } 16 | 17 | public function down() 18 | { 19 | echo "m160921_1102234_remove_has_wall_entry cannot be reverted.\n"; 20 | 21 | return false; 22 | } 23 | 24 | /* 25 | * // Use safeUp/safeDown to run migration code within a transaction public function safeUp() { } public function safeDown() { } 26 | */ 27 | } 28 | -------------------------------------------------------------------------------- /widgets/BreadcrumbBar.php: -------------------------------------------------------------------------------- 1 | render('breadcrumbBar', [ 27 | 'folder' => $this->folder, 28 | 'contentContainer' => $this->contentContainer, 29 | ]); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /extensions/custom_pages/elements/FoldersElementVariable.php: -------------------------------------------------------------------------------- 1 | getItems() as $folder) { 20 | $this->items[] = FolderElementVariable::instance($elementContent)->setRecord($folder); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/codeception/acceptance.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | 3 | # suite for acceptance tests. 4 | # perform tests in browser using the Selenium-like tools. 5 | # powered by Mink (http://mink.behat.org). 6 | # (tip: that's what your customer will see). 7 | # (tip: test your ajax and javascript by one of Mink drivers). 8 | 9 | # RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. 10 | 11 | actor: AcceptanceTester 12 | modules: 13 | enabled: 14 | - WebDriver 15 | - tests\codeception\_support\WebHelper 16 | - tests\codeception\_support\DynamicFixtureHelper 17 | config: 18 | WebDriver: 19 | url: 'http://localhost:8080/' 20 | browser: chrome 21 | restart: true 22 | port: 4444 23 | capabilities: 24 | chromeOptions: 25 | args: ["--lang=en-US"] 26 | -------------------------------------------------------------------------------- /tests/codeception/fixtures/data/file.php: -------------------------------------------------------------------------------- 1 | addColumn('cfiles_folder', 'description', $this->string(1000)); 11 | $this->update('cfiles_folder', ['description' => '']); 12 | $this->addColumn('cfiles_file', 'description', $this->string(1000)); 13 | $this->update('cfiles_file', ['description' => '']); 14 | } 15 | 16 | public function down() 17 | { 18 | echo "m160810_174011_add_description cannot be reverted.\n"; 19 | 20 | return false; 21 | } 22 | 23 | /* 24 | * // Use safeUp/safeDown to run migration code within a transaction public function safeUp() { } public function safeDown() { } 25 | */ 26 | } 27 | -------------------------------------------------------------------------------- /views/move/directory_tree.php: -------------------------------------------------------------------------------- 1 | 17 | 18 |
    19 |
    20 | 25 |
    26 | -------------------------------------------------------------------------------- /views/config-container/index.php: -------------------------------------------------------------------------------- 1 | 10 | 11 |
    12 | 13 |
    Files module configuration'); ?>
    14 | 15 |
    16 | 'configure-form']); ?> 17 | 18 | field($model, 'contentHiddenDefault')->widget(ContentHiddenCheckbox::class, [ 19 | 'type' => ContentHiddenCheckbox::TYPE_CONTENTCONTAINER, 20 | ]); ?> 21 | 22 | submit() ?> 23 | 24 |
    25 |
    26 | -------------------------------------------------------------------------------- /tests/codeception.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | namespace: cfiles 3 | bootstrap: _bootstrap.php 4 | settings: 5 | suite_class: \PHPUnit_Framework_TestSuite 6 | colors: true 7 | shuffle: false 8 | memory_limit: 1024M 9 | log: true 10 | 11 | # This value controls whether PHPUnit attempts to backup global variables 12 | # See https://phpunit.de/manual/current/en/appendixes.annotations.html#appendixes.annotations.backupGlobals 13 | backup_globals: true 14 | paths: 15 | tests: codeception 16 | output: codeception/_output 17 | data: codeception/_data 18 | helpers: codeception/_support 19 | envs: ../../../humhub/tests/config/env 20 | config: 21 | # the entry script URL (with host info) for functional and acceptance tests 22 | # PLEASE ADJUST IT TO THE ACTUAL ENTRY SCRIPT URL 23 | test_entry_url: http://localhost:8080/index-test.php 24 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Files 2 | 3 | Enhance your network's efficiency with the Files Module, a complete solution for easy file management. Seamlessly integrated with HumHub, this module enables you to effortlessly manage and share important files throughout your network and team. 4 | 5 | ## Key Features 6 | 7 | - **File Overview:** Get instant access to all files from your stream and profile. 8 | - **Interact:** Comment on and like files for better collaboration and feedback. 9 | - **Folder Structure:** Organize your files into unlimited folders and subfolders. 10 | - **Migration:** Effortlessly move files and folders within your network. 11 | - **File Info:** Always visible information about the creator, editor, and creation date. 12 | - **Import Files:** Import files and folder structures directly from a .zip file. 13 | - **Export Files:** Download folders and files in bulk as a .zip file. 14 | -------------------------------------------------------------------------------- /migrations/m150720_174011_initial.php: -------------------------------------------------------------------------------- 1 | createTable('cfiles_file', [ 11 | 'id' => 'pk', 12 | 'parent_folder_id' => 'int(11) NULL', 13 | ], ''); 14 | 15 | $this->createTable('cfiles_folder', [ 16 | 'id' => 'pk', 17 | 'parent_folder_id' => 'int(11) NULL', 18 | 'title' => 'varchar(255) NOT NULL', 19 | ], ''); 20 | } 21 | 22 | public function down() 23 | { 24 | echo "m150720_174011_initial cannot be reverted.\n"; 25 | 26 | return false; 27 | } 28 | 29 | /* 30 | * // Use safeUp/safeDown to run migration code within a transaction public function safeUp() { } public function safeDown() { } 31 | */ 32 | } 33 | -------------------------------------------------------------------------------- /tests/codeception/_support/UnitTester.php: -------------------------------------------------------------------------------- 1 | 12 | 13 | Yii::t('CfilesModule.base', 'Move files'), 15 | 'footer' => ModalButton::cancel() . ' ' . ModalButton::save()->submit($model->getMoveUrl()), 16 | ]) ?> 17 | 18 | render('directory_tree', ['root' => $model->root]) ?> 19 | 20 | field($model, 'destId')->hiddenInput(['id' => 'input-hidden-selectedFolder'])->label(false) ?> 21 | 22 | selection as $item) : ?> 23 | 'input-hidden-selectedItem']) ?> 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/codeception/unit/FolderTest.php: -------------------------------------------------------------------------------- 1 | becomeUser('Admin'); 28 | $space = Space::findOne(1); 29 | $rootFolder = Folder::initRoot($space); 30 | 31 | $this->assertTrue($rootFolder instanceof Folder); 32 | // Prevent double root initialization 33 | $this->assertFalse(Folder::initRoot($space)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /views/config/index.php: -------------------------------------------------------------------------------- 1 | 10 | 11 |
    12 | 13 |
    Files module configuration'); ?>
    14 | 15 |
    16 | 'configure-form']); ?> 17 | 18 | field($model, 'disableZipSupport')->checkbox(); ?> 19 | 20 | field($model, 'displayDownloadCount')->checkbox(); ?> 21 | 22 | field($model, 'contentHiddenDefault')->widget(ContentHiddenCheckbox::class, [ 23 | 'type' => ContentHiddenCheckbox::TYPE_GLOBAL, 24 | ]); ?> 25 | 26 | submit() ?> 27 | 28 |
    29 |
    30 | -------------------------------------------------------------------------------- /controllers/ConfigController.php: -------------------------------------------------------------------------------- 1 | load(Yii::$app->request->post()) && $form->save()) { 32 | $this->view->saved(); 33 | } 34 | 35 | return $this->render('index', ['model' => $form]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /widgets/views/folderView.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | $folder, 'contentContainer' => $contentContainer]) ?> 18 | 19 | 'cfiles_progress']) ?> 20 | 21 | $folder, 23 | 'contentContainer' => $contentContainer, 24 | ]) ?> 25 | 26 |
    27 | $folder, 29 | 'contentContainer' => $contentContainer, 30 | ])?> 31 |
    32 | -------------------------------------------------------------------------------- /assets/Assets.php: -------------------------------------------------------------------------------- 1 | =1.5 19 | * 20 | * @var bool 21 | */ 22 | public $defer = true; 23 | 24 | public $publishOptions = [ 25 | 'forceCopy' => false, 26 | ]; 27 | 28 | public $css = [ 29 | 'css/cfiles.css', 30 | 'css/directorylist.css', 31 | ]; 32 | 33 | public $jsOptions = [ 34 | 'position' => \yii\web\View::POS_BEGIN, 35 | ]; 36 | 37 | public $js = [ 38 | 'js/humhub.cfiles.js', 39 | ]; 40 | 41 | public function init() 42 | { 43 | $this->sourcePath = dirname(__FILE__, 2) . '/resources'; 44 | parent::init(); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /module.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cfiles", 3 | "name": "Files", 4 | "description": "Streamline file management across your network and enhance efficiency.", 5 | "keywords": [ 6 | "files", 7 | "docs", 8 | "structure", 9 | "organisation", 10 | "sharing" 11 | ], 12 | "version": "0.17.2", 13 | "humhub": { 14 | "minVersion": "1.18" 15 | }, 16 | "homepage": "https://github.com/humhub/cfiles", 17 | "authors": [ 18 | { 19 | "name": "Sebastian Stumpf" 20 | }, 21 | { 22 | "name": "Lucas Bartholemy" 23 | }, 24 | { 25 | "name": "Julian Harrer" 26 | } 27 | 28 | ], 29 | "screenshots": [ 30 | "resources/screenshot1.png", 31 | "resources/screenshot2.png", 32 | "resources/screenshot3.png", 33 | "resources/screenshot4.png", 34 | "resources/screenshot5.png", 35 | "resources/screenshot6.png", 36 | "resources/screenshot7.png", 37 | "resources/screenshot8.png" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /controllers/ConfigContainerController.php: -------------------------------------------------------------------------------- 1 | [Space::USERGROUP_ADMIN, User::USERGROUP_SELF]]]; 20 | } 21 | 22 | public function actionIndex() 23 | { 24 | $form = new ConfigureContainerForm(['contentContainer' => $this->contentContainer]); 25 | 26 | if ($form->load(Yii::$app->request->post()) && $form->save()) { 27 | $this->view->saved(); 28 | } 29 | 30 | return $this->render('index', ['model' => $form]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /extensions/custom_pages/elements/FilesElement.php: -------------------------------------------------------------------------------- 1 | update('cfiles_file', ['parent_folder_id' => new \yii\db\Expression('NULL')], ['parent_folder_id' => 0]); 17 | $this->update('cfiles_folder', ['parent_folder_id' => new \yii\db\Expression('NULL')], ['parent_folder_id' => 0]); 18 | } 19 | 20 | public function safeDown() 21 | { 22 | echo "m170830_122433_set_root_partent_null.\n"; 23 | 24 | return false; 25 | } 26 | 27 | /* 28 | // Use up()/down() to run migration code without a transaction. 29 | public function up() 30 | { 31 | 32 | } 33 | 34 | public function down() 35 | { 36 | echo "m170830_122432_foreignkeys cannot be reverted.\n"; 37 | 38 | return false; 39 | } 40 | */ 41 | } 42 | -------------------------------------------------------------------------------- /models/ZipImportHandler.php: -------------------------------------------------------------------------------- 1 | Icon::get($this->icon) . Yii::t('CfilesModule.base', 'Import Zip'), 31 | 'data-action-click' => 'file.upload', 32 | 'data-action-target' => '#cfilesUploadZipFile', 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /views/edit/modal_edit_folder.php: -------------------------------------------------------------------------------- 1 | isNewRecord) 12 | ? Yii::t('CfilesModule.base', 'Create folder') 13 | : Yii::t('CfilesModule.base', 'Edit folder'); 14 | ?> 15 | 16 | $header, 18 | 'footer' => ModalButton::cancel() . ' ' . ModalButton::save()->submit($submitUrl), 19 | ]) ?> 20 | 21 | field($folder, 'title')->textInput(['autofocus' => '']) ?> 22 | field($folder, 'description') ?> 23 | field($folder, 'visibility')->widget(ContentVisibilitySelect::class, ['readonly' => !$folder->isRoot() && $folder->parentFolder->content->isPrivate()]) ?> 24 | field($folder, 'hidden')->widget(ContentHiddenCheckbox::class) ?> 25 | 26 | 27 | -------------------------------------------------------------------------------- /migrations/m170210_154141_folderNoStream.php: -------------------------------------------------------------------------------- 1 | dropColumn('cfiles_folder', 'has_wall_entry'); 11 | } catch (Exception) { 12 | Yii::error("Could not drop haswall entry column", 'cfiles'); 13 | } 14 | 15 | $this->db->createCommand('UPDATE content c ' 16 | . 'LEFT JOIN cfiles_folder f ON f.id=c.object_id AND c.object_model=:folderClass ' 17 | . 'SET c.stream_channel = NULL ' 18 | . 'WHERE f.id IS NOT NULL', [':folderClass' => humhub\modules\cfiles\models\Folder::class])->execute(); 19 | } 20 | 21 | public function down() 22 | { 23 | echo "m170210_154141_folderNoStream cannot be reverted.\n"; 24 | 25 | return false; 26 | } 27 | 28 | /* 29 | // Use safeUp/safeDown to run migration code within a transaction 30 | public function safeUp() 31 | { 32 | } 33 | 34 | public function safeDown() 35 | { 36 | } 37 | */ 38 | } 39 | -------------------------------------------------------------------------------- /views/edit/modal_edit_file.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | Yii::t('CfilesModule.base', 'Edit file'), 17 | 'footer' => ModalButton::cancel() . ' ' . ModalButton::save()->submit($submitUrl), 18 | ]) ?> 19 | 20 | field($file->baseFile, 'file_name')->textInput(['autofocus' => '']) ?> 21 | field($file, 'description')->widget(RichTextField::class) ?> 22 | field($file, 'visibility')->widget(ContentVisibilitySelect::class, ['readonly' => $file->parentFolder->content->isPrivate()]) ?> 23 | field($file, 'hidden')->widget(ContentHiddenCheckbox::class, []) ?> 24 | field($file, 'download_count')->staticControl(['style' => 'display:inline']) ?> 25 | 26 | 27 | -------------------------------------------------------------------------------- /widgets/views/versionsView.php: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
    26 | 27 |
    28 |
    29 | icon('chevron-down') 31 | ->action('cfiles.loadNextPageVersions', $nextPageVersionsUrl) 32 | ->sm() ?> 33 |
    34 | 35 | -------------------------------------------------------------------------------- /widgets/views/breadcrumbBar.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | isRoot()): ?> 11 | 31 | 32 | -------------------------------------------------------------------------------- /migrations/m170830_122439_foreignkeys.php: -------------------------------------------------------------------------------- 1 | addForeignKey('fk_cfiles_file_parent_folder', 'cfiles_file', 'parent_folder_id', 'cfiles_folder', 'id', 'SET NULL'); 18 | $this->addForeignKey('fk_cfiles_folder_parent_folder', 'cfiles_folder', 'parent_folder_id', 'cfiles_folder', 'id', 'SET NULL'); 19 | } catch (Exception $e) { 20 | Yii::error($e); 21 | } 22 | } 23 | 24 | public function safeDown() 25 | { 26 | echo "m170830_122437_foreignkeys.\n"; 27 | 28 | return false; 29 | } 30 | 31 | /* 32 | // Use up()/down() to run migration code without a transaction. 33 | public function up() 34 | { 35 | 36 | } 37 | 38 | public function down() 39 | { 40 | echo "m170830_122432_foreignkeys cannot be reverted.\n"; 41 | 42 | return false; 43 | } 44 | */ 45 | } 46 | -------------------------------------------------------------------------------- /models/ItemInterface.php: -------------------------------------------------------------------------------- 1 | request->post('selection'); 36 | 37 | if ($selection === null) { 38 | // Try to get param from GET because REST API method $I->sendDelete() 39 | // sends params as GET params instead of expected BODY params 40 | $selection = Yii::$app->request->get('selection'); 41 | } 42 | 43 | if (is_array($selection)) { 44 | $this->selection = $selection; 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /widgets/views/wallEntryFile.php: -------------------------------------------------------------------------------- 1 | 15 | 16 | applyFile($file)): ?> 17 |
    18 | renderGalleryLink(['style' => 'padding-right:12px']); ?> 19 |
    20 | 21 | 22 | 'text-decoration: underline']); ?>
    23 | Yii::$app->formatter->asShortSize($fileSize, 1)]); ?>
    24 | 25 | description)): ?> 26 |
    27 |
    28 | description, RichText::FORMAT_HTML) ?> 29 |
    30 | 31 | 32 |
    33 | 34 | 35 | 'btn btn-sm btn-light', 'data-ui-loader' => '']); ?> 36 | 37 | 38 |
    39 | -------------------------------------------------------------------------------- /controllers/DownloadController.php: -------------------------------------------------------------------------------- 1 | $guid]); 30 | 31 | if (!$file) { 32 | throw new NotFoundHttpException(); 33 | } 34 | 35 | return $this->redirect($file->getUrl([ 36 | 'download' => 1, // Force downloading even if file can be viewable by browser 37 | 'file_name' => $file->file_name, // used to avoid browser cache when file was renamed 38 | ])); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /controllers/DeleteController.php: -------------------------------------------------------------------------------- 1 | request->post('selection'); 30 | 31 | if (is_array($selectedItems)) { 32 | foreach ($selectedItems as $itemId) { 33 | $item = FileSystemItem::getItemById($itemId); 34 | 35 | if (!$item->content->canEdit()) { 36 | throw new HttpException(403); 37 | } 38 | 39 | if ($item && $item->isDeletable() && $item->content->container->id === $this->contentContainer->id) { 40 | $item->delete(); 41 | } 42 | } 43 | } 44 | 45 | return $this->renderFileList(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/codeception/_bootstrap.php: -------------------------------------------------------------------------------- 1 | $testRoot]); 16 | codecept_debug('Module root: ' . $testRoot); 17 | 18 | $humhubPath = getenv('HUMHUB_PATH'); 19 | if ($humhubPath === false) { 20 | // If no environment path was set, we assume residing in default the modules directory 21 | $moduleConfig = require $testRoot . '/config/test.php'; 22 | if (isset($moduleConfig['humhub_root'])) { 23 | $humhubPath = $moduleConfig['humhub_root']; 24 | } else { 25 | $humhubPath = dirname(__DIR__, 5); 26 | } 27 | } 28 | 29 | \Codeception\Configuration::append(['humhub_root' => $humhubPath]); 30 | codecept_debug('HumHub Root: ' . $humhubPath); 31 | 32 | // Load test configuration (/config/test.php or /config/env//test.php 33 | $globalConfig = require $humhubPath . '/protected/humhub/tests/codeception/_loadConfig.php'; 34 | 35 | // Load default test bootstrap (initialize Yii...) 36 | require $globalConfig['humhub_root'] . '/protected/humhub/tests/codeception/_bootstrap.php'; 37 | -------------------------------------------------------------------------------- /tests/codeception/acceptance/GuestAccessCest.php: -------------------------------------------------------------------------------- 1 | amAdmin(); 19 | $I->allowGuestAccess(); 20 | 21 | $I->amUser1(true); 22 | $I->wantToTest('the visibility of folders and files for guests'); 23 | $I->amGoingTo('install the cfiles module for space 2'); 24 | $I->enableCfilesOnSpace(2); 25 | 26 | $I->amGoingTo('create a public folder and file'); 27 | $I->createFolder('guest', 'guest test', true); 28 | $I->uploadFile('test.txt'); 29 | 30 | $I->amInRoot(); 31 | 32 | $I->createFolder('private', 'private test', false); 33 | $I->uploadFile('test.txt'); 34 | 35 | $I->logout(); 36 | 37 | $I->amOnSpace2(); 38 | $I->waitForText('Files', 10, '.layout-nav-container'); 39 | $I->click('Files', '.layout-nav-container'); 40 | 41 | $I->seeInFileList('guest'); 42 | $I->dontSeeInFileList('private'); 43 | 44 | $I->openFolder('guest'); 45 | $I->seeInFileList('test.txt'); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /widgets/DropdownButton.php: -------------------------------------------------------------------------------- 1 | buttons) > 1) { 19 | foreach ($this->buttons as $button) { 20 | $items[] = '
  • ' . str_replace($this->icon, '', $button) . '
  • '; 21 | } 22 | if ($this->split) { 23 | array_shift($items); 24 | } 25 | return ButtonDropdown::widget([ 26 | 'dropdown' => [ 27 | 'items' => $items, 28 | ], 29 | 'splitButton' => $this->split ? [ 30 | 'visible' => true, 31 | 'options' => $this->options, 32 | ] : null, 33 | 'button' => $this->split ? $this->buttons[0] : [ 34 | 'encodeLabel' => false, 35 | 'label' => $this->icon . $this->label, 36 | 'options' => $this->options, 37 | ], 38 | ]); 39 | } elseif (count($this->buttons) > 0) { 40 | return $this->buttons[0]; 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /permissions/ManageFiles.php: -------------------------------------------------------------------------------- 1 | SORT_ASC]; 25 | 26 | public const ORDER_MAPPING = [ 27 | self::ORDER_TYPE_NAME => 'title', 28 | self::ORDER_TYPE_UPDATED_AT => 'content.updated_at', 29 | self::ORDER_TYPE_SIZE => null, 30 | ]; 31 | 32 | /** 33 | * @var \humhub\modules\cfiles\models\Folder 34 | */ 35 | public $item; 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function getBaseFile() 41 | { 42 | return null; 43 | } 44 | 45 | /** 46 | * @return bool 47 | */ 48 | public function canEdit() 49 | { 50 | return $this->item->content->canEdit(); 51 | } 52 | 53 | /** 54 | * @inheritdoc 55 | */ 56 | public function getContext(): WallStreamModuleEntryWidget 57 | { 58 | return new WallEntryFolder(['model' => $this->item]); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /permissions/WriteAccess.php: -------------------------------------------------------------------------------- 1 | Yii::t('CfilesModule.base', 'Folder content ID'), 41 | ]; 42 | } 43 | 44 | public function __toString(): string 45 | { 46 | return (string) Html::encode($this->record?->title); 47 | } 48 | 49 | /** 50 | * @inheritdoc 51 | */ 52 | public function getTemplateVariable(): BaseElementVariable 53 | { 54 | return FolderElementVariable::instance($this)->setRecord($this->getRecord()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /actions/UploadZipAction.php: -------------------------------------------------------------------------------- 1 | extract($this->controller->getCurrentFolder(), $uploadedFile); 30 | 31 | if ($file->hasErrors()) { 32 | return $this->getValidationErrorResponse($file); 33 | } 34 | 35 | return ['error' => false]; 36 | } 37 | 38 | protected function getValidationErrorResponse(FileSystemItem $file) 39 | { 40 | $errorMessage = Yii::t('FileModule.actions_UploadAction', 'File {fileName} could not be uploaded!', ['fileName' => $file->baseFile->name ?? '']); 41 | 42 | if (!empty($file->hasErrors())) { 43 | $errorMessage = array_values($file->getErrors())[0]; 44 | } 45 | 46 | return [ 47 | 'error' => true, 48 | 'errors' => $errorMessage, 49 | 'name' => $file->baseFile->name ?? '', 50 | 'size' => $file->baseFile->size ?? '', 51 | ]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/codeception/acceptance/ImportZipCest.php: -------------------------------------------------------------------------------- 1 | amAdmin(); 17 | $I->wantToTest('the import a zip file'); 18 | $I->amGoingTo('install the cfiles module for space 1'); 19 | $I->enableCfilesOnSpace(); 20 | 21 | $I->createFolder('test'); 22 | $I->wait(1); 23 | 24 | $I->attachFile('#cfilesUploadFiles', 'test.txt'); 25 | 26 | $I->click('.fa-home', '#cfiles-crumb'); 27 | $I->wait(2); 28 | 29 | $I->attachFile('#cfilesUploadZipFile', 'test.zip'); 30 | 31 | $I->wait(5); 32 | 33 | $I->click('test', '#fileList'); 34 | $I->waitForText('test.txt', 10, '#fileList'); 35 | $I->waitForText('test(1).txt', 10, '#fileList'); 36 | $I->waitForText('test.jpg', 10, '#fileList'); 37 | $I->waitForText('test2', 10, '#fileList'); 38 | 39 | // Disable tooltip from 'test description' of the parent "test" folder which is misplaced during tests 40 | $I->executeJS(" 41 | var tooltips = document.querySelectorAll('.tooltip'); 42 | tooltips.forEach(function(tooltip) { 43 | tooltip.remove(); 44 | }); 45 | "); 46 | $I->click('test2', '#fileList'); 47 | $I->waitForText('test2.txt', 10, '#fileList'); 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /widgets/FileListMenu.php: -------------------------------------------------------------------------------- 1 | contentContainer->can(WriteAccess::class); 43 | 44 | return $this->render('fileListMenu', [ 45 | 'folder' => $this->folder, 46 | 'contentContainer' => $this->contentContainer, 47 | 'canUpload' => $canUpload, 48 | 'fileHandlers' => array_merge($fileHandlerCreate, $fileHandlerImport), 49 | ]); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /models/rows/SpecialFolderRow.php: -------------------------------------------------------------------------------- 1 | null, 25 | self::ORDER_TYPE_UPDATED_AT => null, 26 | self::ORDER_TYPE_SIZE => null, 27 | ]; 28 | 29 | /** 30 | * @inheritdoc 31 | */ 32 | public function isSocialActionsAvailable() 33 | { 34 | return false; 35 | } 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function isSelectable() 41 | { 42 | return false; 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function getCreator() 49 | { 50 | // do not display creator of automatically generated folders 51 | return false; 52 | } 53 | 54 | /** 55 | * @inheritdoc 56 | */ 57 | public function getEditor() 58 | { 59 | // do not display editor of automatically generated folders 60 | return false; 61 | } 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public function getUpdatedAt() 67 | { 68 | return ($this->item->isAllPostedFiles()) ? '' : parent::getUpdatedAt(); 69 | } 70 | 71 | public function canEdit() 72 | { 73 | return false; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /models/ConfigureContainerForm.php: -------------------------------------------------------------------------------- 1 | loadBySettings(); 22 | } 23 | 24 | /** 25 | * @return Module 26 | */ 27 | public function getModule() 28 | { 29 | return Yii::$app->getModule('cfiles'); 30 | } 31 | 32 | /** 33 | * @inheritdoc 34 | */ 35 | public function rules() 36 | { 37 | return [ 38 | [['contentHiddenDefault'], 'boolean'], 39 | ]; 40 | } 41 | 42 | public function loadBySettings() 43 | { 44 | $this->contentHiddenDefault = $this->getSettings()->get( 45 | 'contentHiddenDefault', 46 | $this->getModule()->getContentHiddenGlobalDefault(), 47 | ); 48 | } 49 | 50 | public function save(): bool 51 | { 52 | if (!$this->validate()) { 53 | return false; 54 | } 55 | 56 | $this->getSettings()->set('contentHiddenDefault', $this->contentHiddenDefault); 57 | 58 | return true; 59 | } 60 | 61 | private function getSettings(): ContentContainerSettingsManager 62 | { 63 | return $this->getModule()->settings->contentContainer($this->contentContainer); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /views/browse/index.php: -------------------------------------------------------------------------------- 1 | registerJsConfig('cfiles', [ 13 | 'text' => [ 14 | 'confirm.delete' => Yii::t('CfilesModule.base', 'Do you really want to delete this {number} item(s) with all subcontent?'), 15 | 'confirm.delete.header' => Yii::t('CfilesModule.base', 'Confirm delete file'), 16 | 'confirm.delete.confirmText' => Yii::t('CfilesModule.base', 'Delete') 17 | ], 18 | 'showUrlModal' => [ 19 | 'head' => Yii::t('CfilesModule.base', 'File url'), 20 | 'headFile' => Yii::t('CfilesModule.base', 'File download url'), 21 | 'headFolder' => Yii::t('CfilesModule.base', 'Folder url'), 22 | 'info' => Yii::t('base', 'Copy to clipboard'), 23 | 'buttonClose' => Yii::t('base', 'Close'), 24 | ], 25 | 'reloadEntryUrl' => $contentContainer->createUrl('/cfiles/browse/load-entry'), 26 | ]); 27 | ?> 28 | 29 | '#globalModal', 'id' => 'cfiles-form']); ?> 30 |
    31 | 32 |
    33 | 34 | $contentContainer, 36 | 'folder' => $folder 37 | ])?> 38 | 39 |
    40 |
    41 | 42 | -------------------------------------------------------------------------------- /widgets/views/versionItem.php: -------------------------------------------------------------------------------- 1 | 19 | 20 | formatter->asDatetime($date, 'short') ?> 21 | displayName) ?> 22 | formatter->asShortSize($size, 1) ?> 23 | 24 | 25 | ', $revertUrl, [ 26 | 'title' => Yii::t('CfilesModule.base', 'Revert to this version'), 27 | 'data-method' => 'POST', 28 | ]) ?> 29 | 30 | ', $downloadUrl, [ 31 | 'title' => Yii::t('CfilesModule.base', 'Download'), 32 | 'target' => '_blank', 33 | ]) ?> 34 | 35 | ', $deleteUrl, [ 36 | 'title' => Yii::t('CfilesModule.base', 'Delete this version!'), 37 | 'data-action-confirm' => Yii::t('CfilesModule.base', 'Are you really sure to delete this version?'), 38 | 'data-action-click' => 'cfiles.deleteVersion', 39 | ]) ?> 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /extensions/custom_pages/elements/FolderElementVariable.php: -------------------------------------------------------------------------------- 1 | title = $record->title; 37 | $this->description = $record->description; 38 | $this->type = $record->type; 39 | $this->icon = $record->getIcon(); 40 | 41 | foreach ($record->subFolders as $subFolder) { 42 | $this->subFolders[] = self::instance($this->elementContent)->setRecord($subFolder); 43 | } 44 | 45 | foreach ($record->subFiles as $file) { 46 | $this->subFiles[] = FileElementVariable::instance($this->elementContent)->setRecord($file); 47 | } 48 | } 49 | 50 | return parent::setRecord($record); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /extensions/custom_pages/elements/FileElementVariable.php: -------------------------------------------------------------------------------- 1 | description = $record->description; 35 | $this->downloadCount = (int) $record->download_count; 36 | $this->icon = $record->getIcon(); 37 | 38 | if ($record->baseFile->store->has()) { 39 | $this->fileUrl = $record->baseFile->getUrl(); 40 | $this->file = BaseFileElementVariable::instance($this->elementContent) 41 | ->setRecord($record->baseFile); 42 | } 43 | } 44 | 45 | return parent::setRecord($record); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /widgets/FileSelectionMenu.php: -------------------------------------------------------------------------------- 1 | folder->createUrl('/cfiles/delete'); 40 | $moveSelectionUrl = $this->folder->createUrl('/cfiles/move', ['init' => 1]); 41 | 42 | $zipSelectionUrl = $this->folder->createUrl('/cfiles/zip/download'); 43 | $makePrivateUrl = $this->folder->createUrl('/cfiles/edit/make-private'); 44 | $makePublicUrl = $this->folder->createUrl('/cfiles/edit/make-public'); 45 | 46 | $canWrite = $this->contentContainer->can(ManageFiles::class); 47 | 48 | return $this->render('fileSelectionMenu', [ 49 | 'deleteSelectionUrl' => $deleteSelectionUrl, 50 | 'folder' => $this->folder, 51 | 'moveSelectionUrl' => $moveSelectionUrl, 52 | 'zipSelectionUrl' => $zipSelectionUrl, 53 | 'canWrite' => $canWrite, 54 | 'zipEnabled' => Yii::$app->getModule('cfiles')->isZipSupportEnabled(), 55 | 'makePrivateUrl' => $makePrivateUrl, 56 | 'makePublicUrl' => $makePublicUrl, 57 | ]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | 'cfiles', 14 | 'class' => 'humhub\modules\cfiles\Module', 15 | 'namespace' => 'humhub\modules\cfiles', 16 | 'urlManagerRules' => [ 17 | ['class' => UrlRule::class], 18 | ], 19 | 'events' => [ 20 | [Menu::class, Menu::EVENT_INIT, ['humhub\modules\cfiles\Events', 'onSpaceMenuInit']], 21 | [ProfileMenu::class, ProfileMenu::EVENT_INIT, ['humhub\modules\cfiles\Events', 'onProfileMenuInit']], 22 | [IntegrityController::class, IntegrityController::EVENT_ON_RUN, ['humhub\modules\cfiles\Events', 'onIntegrityCheck']], 23 | [FileController::class, FileController::EVENT_AFTER_ACTION, ['humhub\modules\cfiles\Events', 'onAfterFileAction']], 24 | [File::class, File::EVENT_AFTER_NEW_STORED_FILE, ['humhub\modules\cfiles\Events', 'onAfterNewStoredFile']], 25 | [ContentContainerActiveRecord::class, ContentContainerActiveRecord::EVENT_AFTER_INSERT, ['humhub\modules\cfiles\Events', 'onContentContainerActiveRecordInsert']], 26 | [ContentContainerModuleState::class, ContentContainerModuleState::EVENT_AFTER_INSERT, ['humhub\modules\cfiles\Events', 'onContentContainerModuleStateInsert']], 27 | ['humhub\modules\rest\Module', 'restApiAddRules', ['humhub\modules\cfiles\Events', 'onRestApiAddRules']], 28 | ['humhub\modules\custom_pages\modules\template\services\ElementTypeService', 'init', ['humhub\modules\cfiles\Events', 'onCustomPagesTemplateElementTypeServiceInit']], 29 | ], 30 | ]; 31 | -------------------------------------------------------------------------------- /helpers/RestDefinitions.php: -------------------------------------------------------------------------------- 1 | $folder->id, 27 | 'title' => $folder->title, 28 | 'description' => $folder->description, 29 | ]; 30 | } 31 | 32 | public static function getFolder(Folder $folder) 33 | { 34 | return [ 35 | 'id' => $folder->id, 36 | 'title' => $folder->title, 37 | 'description' => $folder->description, 38 | 'parent_folder_id' => $folder->parent_folder_id, 39 | 'type' => $folder->type, 40 | 'created_at' => $folder->content->created_at, 41 | 'created_by' => UserDefinitions::getUserShort($folder->getOwner()), 42 | 'content' => ContentDefinitions::getContent($folder->content), 43 | ]; 44 | } 45 | 46 | public static function getFile(File $file) 47 | { 48 | return [ 49 | 'id' => $file->id, 50 | 'description' => $file->description, 51 | 'parent_folder' => static::getFolderShort($file->parentFolder), 52 | 'created_at' => $file->content->created_at, 53 | 'created_by' => UserDefinitions::getUserShort($file->getOwner()), 54 | 'content' => ContentDefinitions::getContent($file->content), 55 | ]; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /models/rows/FileRow.php: -------------------------------------------------------------------------------- 1 | SORT_ASC]; 25 | 26 | public const ORDER_MAPPING = [ 27 | self::ORDER_TYPE_NAME => 'file.file_name', 28 | self::ORDER_TYPE_UPDATED_AT => 'file.updated_at', 29 | self::ORDER_TYPE_DOWNLOAD_COUNT => 'cfiles_file.download_count', 30 | self::ORDER_TYPE_SIZE => 'cast(file.size as unsigned)', 31 | ]; 32 | 33 | /** 34 | * @var \humhub\modules\cfiles\models\File 35 | */ 36 | public $item; 37 | 38 | /** 39 | * @inheritdoc 40 | */ 41 | public function getUrl() 42 | { 43 | return $this->item->content->container->createUrl('/cfiles/download', ['guid' => $this->item->baseFile->guid], true); 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | public function getDisplayUrl() 50 | { 51 | return $this->getUrl(); 52 | } 53 | 54 | /** 55 | * @inheritdoc 56 | */ 57 | public function getBaseFile() 58 | { 59 | return $this->item->baseFile; 60 | } 61 | 62 | /** 63 | * @return bool 64 | */ 65 | public function canEdit() 66 | { 67 | return $this->item->content->canEdit(); 68 | } 69 | 70 | /** 71 | * @inheritdoc 72 | */ 73 | public function getContext(): WallStreamModuleEntryWidget 74 | { 75 | return new WallEntryFile(['model' => $this->item]); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /widgets/FolderView.php: -------------------------------------------------------------------------------- 1 | $this->folder->id, 46 | 'upload-url' => $this->folder->createUrl('/cfiles/upload'), 47 | 'reload-file-list-url' => $this->folder->createUrl('/cfiles/browse/file-list'), 48 | 'delete-url' => $this->folder->createUrl('/cfiles/delete'), 49 | 'zip-upload-url' => $this->folder->createUrl('/cfiles/zip/upload'), 50 | 'download-archive-url' => $this->folder->createUrl('/cfiles/zip/download'), 51 | 'move-url' => $this->folder->createUrl('/cfiles/move'), 52 | 'drop-url' => $this->folder->createUrl('/cfiles/move/drop'), 53 | 'import-url' => $this->folder->createUrl('/cfiles/upload/import'), 54 | ]; 55 | } 56 | 57 | /** 58 | * @inheritdoc 59 | */ 60 | public function run() 61 | { 62 | return $this->render('folderView', [ 63 | 'folder' => $this->folder, 64 | 'options' => $this->getOptions(), 65 | 'contentContainer' => $this->contentContainer, 66 | ]); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /libs/ZipUtil.php: -------------------------------------------------------------------------------- 1 | getTempPath()); 30 | } 31 | 32 | /** 33 | * Get the output path of the user specified temporary folder used for packing and unpacking zip data for this user. 34 | * 35 | * @return string @runtime/temp/[guid] 36 | */ 37 | protected function getZipOutputPath() 38 | { 39 | // init output directory 40 | $outputPath = $this->getTempPath(); 41 | $outputPath .= DIRECTORY_SEPARATOR . \Yii::$app->user->guid; 42 | if (!is_dir($outputPath)) { 43 | mkdir($outputPath); 44 | } 45 | 46 | return $outputPath; 47 | } 48 | 49 | /** 50 | * Get the output path of the base temporary folder used for packing and unpacking zip data for all users. 51 | * 52 | * @return string @runtime/temp/[guid] 53 | */ 54 | protected function getTempPath() 55 | { 56 | // init output directory 57 | $outputPath = Yii::getAlias('@runtime/cfiles-temp'); 58 | if (!is_dir($outputPath)) { 59 | mkdir($outputPath); 60 | } 61 | return $outputPath; 62 | } 63 | 64 | /** 65 | * Fixes ZIP location path, removes trailling slash 66 | * 67 | * @param string $path 68 | * @return string the fixed path 69 | */ 70 | protected function fixPath($path) 71 | { 72 | return ltrim($path, DIRECTORY_SEPARATOR); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/codeception/acceptance/VersionCest.php: -------------------------------------------------------------------------------- 1 | amAdmin(); 20 | $I->wantToTest('the file versioning'); 21 | $I->amGoingTo('install the cfiles module for space 1'); 22 | $I->enableCfilesOnSpace(); 23 | 24 | $I->attachFile('#cfilesUploadFiles', 'test.txt'); 25 | $I->waitForText('test.txt', 10, '#fileList'); 26 | $I->wait(1); 27 | 28 | $firstVersionFile = $this->getFile(1); 29 | $I->seeFileSizeOnSpaceStream($firstVersionFile); 30 | $I->amOnFilesBrowser(); 31 | 32 | $I->wantToTest('the keep old version'); 33 | $I->attachFile('#cfilesUploadFiles', 'version/test.txt'); 34 | $I->wait(1); 35 | 36 | $secondVersionFile = $this->getFile(1); 37 | $I->seeFileSizeOnSpaceStream($secondVersionFile); 38 | $I->amOnFilesBrowser(); 39 | 40 | $I->amGoingTo('view file versions'); 41 | $I->seeElement('[data-cfiles-item="file_1"]'); 42 | $I->wait(1); 43 | $I->clickFileContext(1, 'Versions'); 44 | $I->waitForText('File versions', 10, '#globalModal'); 45 | 46 | $I->jsClick('#version_file_' . $firstVersionFile->id . ' [title="Revert to this version"]'); 47 | $I->seeSuccess('File ' . $firstVersionFile->file_name . ' has been reverted to version from ' . Yii::$app->formatter->asDatetime($firstVersionFile->created_at, 'short')); 48 | $I->seeFileSizeOnSpaceStream($firstVersionFile); 49 | } 50 | 51 | private function getFile(int $id): BaseFile 52 | { 53 | return BaseFile::findOne(['id' => $id]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/codeception/api/FileCest.php: -------------------------------------------------------------------------------- 1 | isRestModuleEnabled()) { 13 | return; 14 | } 15 | 16 | $I->wantTo('find by container'); 17 | $I->amAdmin(); 18 | $I->seePaginationFilesResponse('cfiles/files/container/1', []); 19 | 20 | $I->createSampleFile(); 21 | $I->seePaginationFilesResponse('cfiles/files/container/1', [1]); 22 | } 23 | 24 | public function testUploadFiles(ApiTester $I) 25 | { 26 | if (!$this->isRestModuleEnabled()) { 27 | return; 28 | } 29 | 30 | $I->wantTo('upload files'); 31 | $I->amAdmin(); 32 | 33 | $I->createFolder('Root'); 34 | $I->seeLastCreatedFolderDefinition(); 35 | 36 | $I->uploadFiles(['test.txt', 'test.zip']); 37 | $I->seeSuccessMessage('Files successfully uploaded!'); 38 | } 39 | 40 | public function testGetFileById(ApiTester $I) 41 | { 42 | if (!$this->isRestModuleEnabled()) { 43 | return; 44 | } 45 | 46 | $I->wantTo('get file by id'); 47 | $I->amAdmin(); 48 | 49 | $I->createSampleFile(); 50 | $I->sendGet('cfiles/file/1'); 51 | $I->seeFileDefinitionById(1); 52 | } 53 | 54 | public function testDeleteFileById(ApiTester $I) 55 | { 56 | if (!$this->isRestModuleEnabled()) { 57 | return; 58 | } 59 | 60 | $I->wantTo('delete file by id'); 61 | $I->amAdmin(); 62 | 63 | $I->sendDelete('cfiles/file/1'); 64 | $I->seeNotFoundMessage('Content record not found!'); 65 | 66 | $I->createSampleFile(); 67 | $I->sendDelete('cfiles/file/1'); 68 | $I->seeSuccessMessage('Successfully deleted!'); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /widgets/WallEntryFolder.php: -------------------------------------------------------------------------------- 1 | render('wallEntryFolder', [ 42 | 'folder' => $this->model, 43 | 'folderUrl' => $this->model->getUrl(), 44 | ]); 45 | } 46 | 47 | /** 48 | * Returns the edit url to edit the content (if supported) 49 | * 50 | * @return string url 51 | */ 52 | public function getEditUrl() 53 | { 54 | if (empty(parent::getEditUrl())) { 55 | return ''; 56 | } 57 | 58 | if ($this->model instanceof Folder) { 59 | return $this->model->content->container->createUrl($this->editRoute, ['id' => $this->model->getItemId(), 'fromWall' => true]); 60 | } 61 | 62 | return ''; 63 | } 64 | 65 | /** 66 | * @return string 67 | */ 68 | protected function getIcon() 69 | { 70 | return $this->model->getIcon(); 71 | } 72 | 73 | /** 74 | * @return string a non encoded plain text title (no html allowed) used in the header of the widget 75 | */ 76 | protected function getTitle() 77 | { 78 | return $this->model->getTitle(); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /widgets/FileSystemItem.php: -------------------------------------------------------------------------------- 1 | row->showSelect = $this->itemsSelectable; 46 | 47 | return $this->render('fileSystemItem', [ 48 | 'folder' => $this->folder, 49 | 'row' => $this->row, 50 | 'options' => $this->getOptions(), 51 | ]); 52 | } 53 | 54 | public function getData() 55 | { 56 | return [ 57 | 'cfiles-item' => $this->row->getItemId(), 58 | 'cfiles-content' => $this->row->getContentId(), 59 | 'cfiles-type' => $this->row->getType(), 60 | 'cfiles-url' => $this->row->getUrl(), 61 | 'cfiles-editable' => $this->row->canEdit(), 62 | 'cfiles-url-full' => $this->row->getDisplayUrl(), 63 | 'cfiles-wall-url' => $this->row->getWallUrl(), 64 | 'cfiles-edit-url' => ($this->row->canEdit()) ? $this->row->getEditUrl() : '', 65 | 'cfiles-move-url' => ($this->row->canEdit()) ? $this->row->getMoveUrl() : '', 66 | 'cfiles-versions-url' => ($this->row->canEdit()) ? $this->row->getVersionsUrl() : '', 67 | ]; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /models/ConfigureForm.php: -------------------------------------------------------------------------------- 1 | getModule(); 26 | $this->disableZipSupport = !$module->isZipSupportEnabled(); 27 | $this->displayDownloadCount = $module->getDisplayDownloadCount(); 28 | $this->contentHiddenDefault = $module->getContentHiddenGlobalDefault(); 29 | } 30 | 31 | /** 32 | * @return Module 33 | */ 34 | public function getModule() 35 | { 36 | return Yii::$app->getModule('cfiles'); 37 | } 38 | 39 | /** 40 | * @inheritdoc 41 | */ 42 | public function rules() 43 | { 44 | return [ 45 | [['disableZipSupport', 'displayDownloadCount', 'contentHiddenDefault'], 'boolean'], 46 | ]; 47 | } 48 | 49 | /** 50 | * @inheritdoc 51 | */ 52 | public function attributeLabels() 53 | { 54 | return [ 55 | 'disableZipSupport' => Yii::t('CfilesModule.base', 'Disable archive (ZIP) support'), 56 | 'displayDownloadCount' => Yii::t('CfilesModule.base', 'Display a download count column'), 57 | ]; 58 | } 59 | 60 | public function save(): bool 61 | { 62 | if (!$this->validate()) { 63 | return false; 64 | } 65 | 66 | $module = $this->getModule(); 67 | $module->settings->set('disableZipSupport', $this->disableZipSupport); 68 | $module->settings->set('displayDownloadCount', $this->displayDownloadCount); 69 | $module->settings->set('contentHiddenGlobalDefault', $this->contentHiddenDefault); 70 | 71 | return true; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/codeception/acceptance/FileContextCest.php: -------------------------------------------------------------------------------- 1 | amAdmin(); 19 | $I->wantToTest('the folder context menu'); 20 | $I->amGoingTo('install the cfiles module for space 1'); 21 | $I->enableCfilesOnSpace(); 22 | 23 | $I->uploadFile(); 24 | $I->wait(1); 25 | 26 | $I->amGoingTo('edit my file per context menu'); 27 | $I->clickFileContext(1, 'Edit'); 28 | $I->waitForText('Edit file', 10, '#globalModal'); 29 | $I->fillField('File[file_name]', 'newFile.txt'); 30 | $I->click('Save', '#globalModal'); 31 | 32 | $I->seeInFileList('newFile.txt'); 33 | 34 | $I->amGoingTo('move my file per context menu'); 35 | $I->createFolder('move'); 36 | $I->amInRoot(); 37 | $I->clickFileContext(1, 'Move'); 38 | $I->waitForText('Move files', 10, '#globalModal'); 39 | $I->click('[data-id="3"]'); 40 | $I->click('Save', '#globalModal'); 41 | $I->seeInCrumb('move'); 42 | $I->seeInFileList('newFile.txt'); 43 | 44 | $I->amGoingTo('delete my file per context menu'); 45 | $I->seeElement('[data-cfiles-item="file_1"]'); 46 | $I->clickFileContext(1, 'Delete'); 47 | $I->waitForElementVisible('#globalModalConfirm', 5); 48 | $I->see('Confirm delete file'); 49 | $I->wait(1); 50 | $I->click('Delete', '#globalModalConfirm'); 51 | $I->waitForElementNotVisible('[data-cfiles-item="file_1"]'); 52 | 53 | $I->amGoingTo('show post of my file'); 54 | $I->uploadFile(); 55 | $I->clickFileContext(2, 'Show Post'); 56 | $I->waitForElementVisible('#wallStream'); 57 | $I->waitForText('test.txt', 10, '[data-stream-entry]'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/codeception/acceptance/UploadAndMoveCest.php: -------------------------------------------------------------------------------- 1 | amAdmin(); 19 | $I->wantToTest('the upload entry'); 20 | $I->amGoingTo('install the cfiles module for space 1'); 21 | $I->enableCfilesOnSpace(); 22 | 23 | $I->attachFile('#cfilesUploadFiles', 'test.txt'); 24 | $I->waitForText('test.txt', 10, '#fileList'); 25 | 26 | $I->wantToTest('the duplicate names entry'); 27 | $I->attachFile('#cfilesUploadFiles', 'test.txt'); 28 | $I->waitForText('test.txt', 10, '#fileList'); 29 | 30 | $I->wantToTest('the creation of a folder'); 31 | $I->click('Add directory', '.files-action-menu'); 32 | 33 | $I->waitForText('Create folder', 10, '#globalModal'); 34 | $I->fillField('Folder[title]', 'NewFolder'); 35 | $I->click('Save', '#globalModal'); 36 | 37 | $I->waitForText('This folder is empty.'); 38 | $I->click('.fa-home', '#cfiles-crumb'); 39 | 40 | $I->waitForText('NewFolder', 10, '#fileList'); 41 | 42 | $I->wantToTest('to move a file into my new folder'); 43 | 44 | $I->jsClick('.allselect'); 45 | $I->click('.chkCnt', '.files-action-menu'); 46 | $I->click('.filemove-button', '.files-action-menu'); 47 | 48 | $I->waitForText('Move files', 10, '#globalModal'); 49 | $I->click('[data-id="3"]'); 50 | $I->click('Save', '#globalModal'); 51 | $I->seeError('Some files could not be moved: Folder NewFolder can\'t be moved to itself!'); 52 | 53 | $I->see('NewFolder', '#fileList'); 54 | $I->dontSee('test.txt', '#fileList'); 55 | 56 | $I->click('NewFolder', '#fileList'); 57 | $I->waitForText('test.txt', 10, '#fileList'); 58 | $I->see('test.txt', '#fileList'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /widgets/VersionItem.php: -------------------------------------------------------------------------------- 1 | isCurrent()) { 47 | $rowOptions = ['class' => 'bg-warning']; 48 | } else { 49 | $rowOptions = ['id' => 'version_file_' . $this->version->id]; 50 | } 51 | 52 | return $this->render('versionItem', [ 53 | 'options' => $rowOptions, 54 | 'user' => $this->getUser(), 55 | 'date' => $this->getDate(), 56 | 'size' => $this->getSize(), 57 | 'revertUrl' => $this->revertUrl, 58 | 'downloadUrl' => $this->downloadUrl, 59 | 'deleteUrl' => $this->deleteUrl, 60 | ]); 61 | } 62 | 63 | private function isCurrent(): bool 64 | { 65 | return $this->version instanceof BaseFile; 66 | } 67 | 68 | private function getUser(): User 69 | { 70 | return $this->isCurrent() ? $this->version->updatedBy : $this->version->createdBy; 71 | } 72 | 73 | private function getDate(): string 74 | { 75 | return $this->isCurrent() ? $this->version->updated_at : $this->version->created_at; 76 | } 77 | 78 | private function getSize(): string 79 | { 80 | return $this->version->size; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /actions/UploadAction.php: -------------------------------------------------------------------------------- 1 | controller->renderFileList(); 39 | return $result; 40 | } 41 | 42 | protected function handleFileUpload(UploadedFile $uploadedFile, $hideInStream = false) 43 | { 44 | $folder = $this->controller->getCurrentFolder(); 45 | 46 | $file = $folder->addUploadedFile($uploadedFile); 47 | 48 | if ($file->hasErrors()) { 49 | return $this->getValidationErrorResponse($file); 50 | } 51 | 52 | if ($file->baseFile->hasErrors()) { 53 | return $this->getErrorResponse($file->baseFile); 54 | } 55 | 56 | return array_merge(['error' => false], FileHelper::getFileInfos($file->baseFile)); 57 | } 58 | 59 | protected function getValidationErrorResponse(FileSystemItem $file) 60 | { 61 | $errorMessage = Yii::t('FileModule.actions_UploadAction', 'File {fileName} could not be uploaded!', ['fileName' => $file->baseFile->name ?? '']); 62 | 63 | if (!empty($file->hasErrors())) { 64 | $errorMessage = $file->getErrorSummary(false); 65 | } 66 | 67 | return [ 68 | 'error' => true, 69 | 'errors' => $errorMessage, 70 | 'name' => $file->baseFile->name ?? '', 71 | 'size' => $file->baseFile->size ?? '', 72 | ]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /components/UrlRule.php: -------------------------------------------------------------------------------- 1 | amAdmin(); 19 | $I->wantToTest('the visibility of folders'); 20 | $I->amGoingTo('install the cfiles module for space 1'); 21 | $I->enableCfilesOnSpace(); 22 | 23 | $I->uploadFile(); 24 | $I->createFolder('test'); 25 | 26 | $I->amInRoot(); 27 | $I->jsClick('.allselect'); 28 | $I->click('.chkCnt', '.files-action-menu'); 29 | $I->click('Delete', '.files-action-menu'); 30 | $I->waitForText('Confirm delete file', 10, '#globalModalConfirm'); 31 | $I->wait(1); 32 | $I->click('Delete', '#globalModalConfirm'); 33 | 34 | $I->waitForElementNotVisible('[data-cfiles-item="file_1"]'); 35 | $I->waitForElementNotVisible('[data-cfiles-item="folder_3"]'); 36 | 37 | $I->uploadFile(); 38 | $I->createFolder('test'); 39 | 40 | $I->amInRoot(); 41 | $I->jsClick('.allselect'); 42 | $I->click('.chkCnt', '.files-action-menu'); 43 | $I->click('Move', '.files-action-menu'); 44 | $I->waitForText('Move files', 10, '#globalModal'); 45 | $I->click('[data-id="4"]'); 46 | $I->click('Save', '#globalModal'); 47 | $I->seeError('Some files could not be moved: Folder test can\'t be moved to itself!'); 48 | $I->seeInFileList('test'); 49 | $I->dontSeeInFileList('test.txt'); 50 | $I->openFolder('test'); 51 | 52 | // Move back to root 53 | $I->jsClick('.allselect'); 54 | $I->click('.chkCnt', '.files-action-menu'); 55 | $I->click('Move', '.files-action-menu'); 56 | $I->waitForText('Move files', 10, '#globalModal'); 57 | $I->click('[data-id="1"]'); 58 | $I->click('Save', '#globalModal'); 59 | $I->wait(2); 60 | $I->seeInFileList('test.txt'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/codeception/acceptance/FolderContextCest.php: -------------------------------------------------------------------------------- 1 | amAdmin(); 19 | $I->wantToTest('the visibility of folders'); 20 | $I->amGoingTo('install the cfiles module for space 1'); 21 | $I->enableCfilesOnSpace(); 22 | 23 | $I->createFolder('context', 'context test'); 24 | $I->uploadFile(); 25 | 26 | $I->amInRoot(); 27 | 28 | $I->amGoingTo('test the folder open context item'); 29 | $I->clickFolderContext(3, 'Open'); 30 | $I->seeInCrumb('context'); 31 | 32 | $I->amInRoot(); 33 | 34 | $I->amGoingTo('test the folder edit context item'); 35 | $I->clickFolderContext(3, 'Edit'); 36 | $I->waitForText('Edit folder', 10, '#globalModal'); 37 | $I->fillField('Folder[title]', 'context2'); 38 | $I->click('Save', '#globalModal'); 39 | 40 | $I->uploadFile(); 41 | 42 | $I->amInRoot(); 43 | $I->seeInFileList('context2'); 44 | 45 | $I->amGoingTo('test the folder move context item'); 46 | 47 | $I->createFolder('context1', 'context test'); 48 | $I->amInRoot(); 49 | $I->clickFolderContext(3, 'Move'); 50 | 51 | $I->waitForText('Move files', 10, '#globalModal'); 52 | $I->click('[data-id="4"]'); 53 | $I->click('Save', '#globalModal'); 54 | 55 | $I->seeInCrumb('context1'); 56 | $I->seeInFileList('context2'); 57 | $I->openFolder('context2'); 58 | $I->seeInFileList('test.txt'); 59 | 60 | $I->amGoingTo('test the folder delete context item'); 61 | $I->amInRoot(); 62 | $I->seeElement('[data-cfiles-item="folder_4"]'); 63 | $I->clickFolderContext(4, 'Delete'); 64 | $I->waitForElementVisible('#globalModalConfirm', 5); 65 | $I->see('Confirm delete file'); 66 | $I->click('Delete', '#globalModalConfirm'); 67 | $I->waitForElementNotVisible('[data-cfiles-item="folder_4"]'); 68 | 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/codeception/api/ManageCest.php: -------------------------------------------------------------------------------- 1 | isRestModuleEnabled()) { 13 | return; 14 | } 15 | 16 | $I->wantTo('make items as public'); 17 | $I->amAdmin(); 18 | $I->createSampleFile(); 19 | 20 | $I->sendPatch('cfiles/items/container/1/make-public', [ 21 | 'selection' => ['folder_1', 'file_1'], 22 | ]); 23 | $I->seeSuccessMessage('Items successfully marked public!'); 24 | } 25 | 26 | public function testMakePrivate(ApiTester $I) 27 | { 28 | if (!$this->isRestModuleEnabled()) { 29 | return; 30 | } 31 | 32 | $I->wantTo('make items as private'); 33 | $I->amAdmin(); 34 | $I->createSampleFile(); 35 | 36 | $I->sendPatch('cfiles/items/container/1/make-private', [ 37 | 'selection' => ['folder_1', 'file_1'], 38 | ]); 39 | $I->seeSuccessMessage('Items successfully marked private!'); 40 | } 41 | 42 | public function testMoveDelete(ApiTester $I) 43 | { 44 | if (!$this->isRestModuleEnabled()) { 45 | return; 46 | } 47 | 48 | $I->wantTo('move and delete items'); 49 | $I->amAdmin(); 50 | 51 | $I->createFolder('First folder'); // folder id = 2, (root folder id = 1) 52 | $I->createFolder('Sub folder 1', ['target_id' => 2]); // folder id = 3 53 | $I->uploadFiles(['test.txt', 'test.zip'], ['folder_id' => 3]); // file ids = 1, 2 54 | $I->createFolder('Sub folder 2', ['target_id' => 2]); // folder id = 4 55 | $I->createFolder('Sub-sub folder 1', ['target_id' => 3]); // folder id = 5 56 | 57 | $I->sendPost('cfiles/items/container/1/move', [ 58 | 'source_id' => 5, 59 | 'MoveForm' => ['destId' => 2], 60 | 'selection' => ['folder_4', 'file_1', 'file_2'], 61 | ]); 62 | $I->seeSuccessMessage('Items successfully moved.'); 63 | 64 | $I->sendDelete('cfiles/items/container/1/delete', [ 65 | 'selection' => ['file_1', 'file_2', 'folder_5', 'folder_4', 'folder_3', 'folder_2'], 66 | ]); 67 | $I->seeSuccessMessage('Selected items are successfully deleted!'); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /messages/en-GB/base.php: -------------------------------------------------------------------------------- 1 | '', 4 | '%filename% has invalid extension and was skipped.' => '', 5 | '%title% was replaced by a newer version.' => '', 6 | '/ (root)' => '', 7 | 'Confirm delete file' => '', 8 | 'Create folder' => '', 9 | 'Edit folder' => '', 10 | 'Files module configuration' => '', 11 | 'Move files' => '', 12 | 'A folder with this name already exists.' => '', 13 | 'Add directory' => '', 14 | 'Add file(s)' => '', 15 | 'Adds files module to this space.' => '', 16 | 'Adds files module to your profile.' => '', 17 | 'Archive %filename% could not be extracted.' => '', 18 | 'Archive (zip) support is not enabled.' => '', 19 | 'Close' => '', 20 | 'Could not save file %title%. ' => '', 21 | 'Creator' => '', 22 | 'Delete' => '', 23 | 'Disable archive (ZIP) support' => '', 24 | 'Do you really want to delete this %number% item(s) with all subcontent?' => '', 25 | 'Download' => '', 26 | 'Download ZIP' => '', 27 | 'Edit' => '', 28 | 'Edit directory' => '', 29 | 'File' => '', 30 | 'Files' => '', 31 | 'Files from the stream' => '', 32 | 'Folder' => '', 33 | 'Folder options' => '', 34 | 'Folder should not start or end with blank space.' => '', 35 | 'Insufficient rights to execute this action.' => '', 36 | 'Invalid parameter.' => '', 37 | 'Move' => '', 38 | 'Moving to the same folder is not valid. Choose a valid parent folder for %title%.' => '', 39 | 'Name' => '', 40 | 'No valid items were selected to move.' => '', 41 | 'Open' => '', 42 | 'Opening archive failed with error code %code%.' => '', 43 | 'Please select a valid destination folder for %title%.' => '', 44 | 'Save' => '', 45 | 'Selected items...' => '', 46 | 'Show' => '', 47 | 'Show Post' => '', 48 | 'Size' => '', 49 | 'The archive could not be created.' => '', 50 | 'The folder %filename% already exists. Contents have been overwritten.' => '', 51 | 'The folder with the id %id% does not exist.' => '', 52 | 'This folder is empty.' => '', 53 | 'Unfortunately you have no permission to upload/edit files.' => '', 54 | 'Updated' => '', 55 | 'Upload' => '', 56 | 'Upload ZIP' => '', 57 | 'Upload files or create a subfolder with the buttons on the top.' => '', 58 | 'Upload files to the stream to fill this folder.' => '', 59 | 'ZIP all' => '', 60 | 'ZIP selected' => '', 61 | 'changed:' => '', 62 | 'created:' => '', 63 | 'root' => '', 64 | ]; 65 | -------------------------------------------------------------------------------- /controllers/ZipController.php: -------------------------------------------------------------------------------- 1 | [WriteAccess::class], 'actions' => ['upload']], 33 | ]; 34 | } 35 | 36 | public function actions() 37 | { 38 | return [ 39 | 'upload' => [ 40 | 'class' => UploadZipAction::class, 41 | ], 42 | ]; 43 | } 44 | 45 | public function checkZipSupport($rule, $access) 46 | { 47 | if (!$this->module->isZipSupportEnabled()) { 48 | $access->code = 404; 49 | $access->reason = Yii::t('CfilesModule.base', 'ZIP support is not enabled.'); 50 | return false; 51 | } 52 | 53 | return true; 54 | } 55 | 56 | /** 57 | * Action to download a zip of the selected items. 58 | */ 59 | public function actionDownload() 60 | { 61 | $selectedItems = Yii::$app->request->post('selection'); 62 | 63 | $items = []; 64 | // Download only the selected items if at least one is selected 65 | if (is_array($selectedItems)) { 66 | foreach ($selectedItems as $itemId) { 67 | $item = FileSystemItem::getItemById($itemId); 68 | if ($item !== null) { 69 | $items[] = $item; 70 | } 71 | } 72 | } 73 | // Otherwise fallback to current folder when no items are selected 74 | if ($items === []) { 75 | $items[] = $this->getCurrentFolder(); 76 | } 77 | 78 | $zip = new ZIPCreator(); 79 | foreach ($items as $item) { 80 | $zip->add($item); 81 | } 82 | $zip->close(); 83 | 84 | return Yii::$app->response->sendFile($zip->getZipFile(), (count($items) == 1) ? $items[0]->title . '.zip' : 'files.zip'); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /models/forms/VersionForm.php: -------------------------------------------------------------------------------- 1 | getFileVersion()) { 51 | $this->addError($attribute, 'The selected version doesn\'t exist for the File!'); 52 | } 53 | } 54 | 55 | public function attributeHints(): array 56 | { 57 | return [ 58 | 'version' => Yii::t('CfilesModule.base', 'Select what file version you want to switch.'), 59 | ]; 60 | } 61 | 62 | public function getFileVersion(): ?FileHistory 63 | { 64 | return $this->file->baseFile->getFileHistoryById($this->version); 65 | } 66 | 67 | /** 68 | * @inheritdoc 69 | */ 70 | public function load($data = null, $formName = null) 71 | { 72 | if ((int)Yii::$app->request->get('version') > 0) { 73 | $data = Yii::$app->request->get(); 74 | $formName = ''; 75 | } else { 76 | $data = Yii::$app->request->post(); 77 | } 78 | 79 | return parent::load($data, $formName); 80 | } 81 | 82 | /** 83 | * Switch the File to a selected version 84 | * 85 | * @return bool 86 | */ 87 | public function save(): bool 88 | { 89 | if (!$this->validate()) { 90 | return false; 91 | } 92 | 93 | $this->file->baseFile->setStoredFile($this->getFileVersion()->getFileStorePath()); 94 | 95 | return $this->file->baseFile->save() && $this->file->refresh(); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /tests/codeception/api/FolderCest.php: -------------------------------------------------------------------------------- 1 | isRestModuleEnabled()) { 13 | return; 14 | } 15 | 16 | $I->wantTo('find by container'); 17 | $I->amAdmin(); 18 | $I->seePaginationFoldersResponse('cfiles/folders/container/1', []); 19 | 20 | $I->createSampleFolder(); 21 | $I->seePaginationFoldersResponse('cfiles/folders/container/1', [1, 2]); 22 | } 23 | 24 | public function testCreateFolder(ApiTester $I) 25 | { 26 | if (!$this->isRestModuleEnabled()) { 27 | return; 28 | } 29 | 30 | $I->wantTo('create a folder'); 31 | $I->amAdmin(); 32 | 33 | $I->createFolder('New folder'); 34 | $I->seeLastCreatedFolderDefinition(); 35 | } 36 | 37 | public function testGetFolderById(ApiTester $I) 38 | { 39 | if (!$this->isRestModuleEnabled()) { 40 | return; 41 | } 42 | 43 | $I->wantTo('get a folder by id'); 44 | $I->amAdmin(); 45 | 46 | $I->createSampleFolder(); 47 | $I->sendGet('cfiles/folder/2'); 48 | $I->seeFolderDefinitionById(2); 49 | } 50 | 51 | public function testUpdateFolder(ApiTester $I) 52 | { 53 | if (!$this->isRestModuleEnabled()) { 54 | return; 55 | } 56 | 57 | $I->wantTo('update a folder'); 58 | $I->amAdmin(); 59 | 60 | $I->sendPut('cfiles/folder/2'); 61 | $I->seeNotFoundMessage('cFiles folder not found!'); 62 | 63 | $I->createSampleFolder(); 64 | $I->sendPut('cfiles/folder/2', [ 65 | 'Folder' => [ 66 | 'title' => 'Updated title', 67 | 'description' => 'Updated description', 68 | 'visibility' => 1, 69 | ], 70 | ]); 71 | $I->seeFolderDefinitionById(2); 72 | } 73 | 74 | public function testDeleteFolder(ApiTester $I) 75 | { 76 | if (!$this->isRestModuleEnabled()) { 77 | return; 78 | } 79 | 80 | $I->wantTo('delete a folder'); 81 | $I->amAdmin(); 82 | 83 | $I->sendDelete('cfiles/folder/2'); 84 | $I->seeNotFoundMessage('Content record not found!'); 85 | 86 | $I->createSampleFolder(); 87 | $I->sendDelete('cfiles/folder/2'); 88 | $I->seeSuccessMessage('Successfully deleted!'); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /extensions/custom_pages/elements/FileElement.php: -------------------------------------------------------------------------------- 1 | Yii::t('CfilesModule.base', 'File content ID'), 43 | ]; 44 | } 45 | 46 | public function __toString(): string 47 | { 48 | if ($this->hasFile()) { 49 | return $this->getFile()->getUrl(); 50 | } 51 | 52 | return (string) Html::encode($this->record?->description); 53 | } 54 | 55 | /** 56 | * @inheritdoc 57 | */ 58 | public function getTemplateVariable(): BaseElementVariable 59 | { 60 | return FileElementVariable::instance($this)->setRecord($this->getRecord()); 61 | } 62 | 63 | /** 64 | * Get File 65 | * 66 | * @return BaseFile|null 67 | */ 68 | public function getFile(): ?BaseFile 69 | { 70 | return empty($this->contentId) ? null 71 | : BaseFile::find() 72 | ->innerJoin(Content::tableName(), Content::tableName() . '.object_model = ' . BaseFile::tableName() . '.object_model' . ' AND ' . Content::tableName() . '.object_id = ' . BaseFile::tableName() . '.object_id') 73 | ->where([BaseFile::tableName() . '.object_model' => File::class]) 74 | ->andWhere([Content::tableName() . '.id' => $this->contentId]) 75 | ->one(); 76 | } 77 | 78 | /** 79 | * Check if a File is found for this Element 80 | * 81 | * @return bool 82 | */ 83 | public function hasFile(): bool 84 | { 85 | return $this->getFile() !== null; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /widgets/WallEntryFile.php: -------------------------------------------------------------------------------- 1 | model; 43 | 44 | return $this->render('wallEntryFile', [ 45 | 'cFile' => $cFile, 46 | 'fileSize' => $cFile->getSize(), 47 | 'file' => $cFile->baseFile, 48 | 'previewImage' => new PreviewImage(), 49 | 'folderUrl' => $this->getFolderUrl(), 50 | ]); 51 | } 52 | 53 | /** 54 | * Returns the edit url to edit the content (if supported) 55 | * 56 | * @return string url 57 | */ 58 | public function getEditUrl() 59 | { 60 | if (empty(parent::getEditUrl())) { 61 | return ''; 62 | } 63 | 64 | if ($this->model instanceof File) { 65 | return $this->model->content->container->createUrl($this->editRoute, ['id' => $this->model->getItemId(), 'fromWall' => true]); 66 | } 67 | 68 | return ''; 69 | } 70 | 71 | /** 72 | * @return string 73 | */ 74 | protected function getIcon() 75 | { 76 | return $this->model->getIcon(); 77 | } 78 | 79 | /** 80 | * @return string a non encoded plain text title (no html allowed) used in the header of the widget 81 | */ 82 | protected function getTitle() 83 | { 84 | return $this->model->getTitle(); 85 | } 86 | 87 | protected function getFolderUrl(): ?string 88 | { 89 | if (!$this->model->parentFolder instanceof Folder) { 90 | return null; 91 | } 92 | 93 | if ($this->model->parentFolder->content->getStateService()->isDeleted()) { 94 | return null; 95 | } 96 | 97 | return $this->model->parentFolder->getUrl(); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /controllers/rest/FileController.php: -------------------------------------------------------------------------------- 1 | $containerId]); 44 | if ($containerRecord === null) { 45 | return $this->returnError(404, 'Content container not found!'); 46 | } 47 | /** @var ContentContainerActiveRecord $container */ 48 | $container = $containerRecord->getPolymorphicRelation(); 49 | 50 | $folderId = Yii::$app->request->post('folder_id', null); 51 | if (is_null($folderId)) { 52 | return $this->returnError(400, 'Target folder id is required!'); 53 | } 54 | 55 | $targetDir = Folder::find()->contentContainer($container)->andWhere(['cfiles_folder.id' => $folderId])->one(); 56 | if ($targetDir === null) { 57 | return $this->returnError(404, 'cFiles folder not found!'); 58 | } 59 | if (!$container->can(ManageFiles::class)) { 60 | return $this->returnError(403, 'You cannot upload files into this folder!'); 61 | } 62 | 63 | $files = UploadedFile::getInstancesByName('files'); 64 | 65 | if (empty($files)) { 66 | return $this->returnError(400, 'No files to upload.'); 67 | } 68 | 69 | foreach ($files as $file) { 70 | $file = $targetDir->addUploadedFile($file); 71 | 72 | if ($file->hasErrors() || $file->baseFile->hasErrors()) { 73 | return $this->returnError(422, "File {$file->baseFile->name} could not be uploaded!", [ 74 | 'errors' => array_merge($file->getErrors(), $file->baseFile->getErrors()), 75 | ]); 76 | } 77 | } 78 | 79 | return $this->returnSuccess('Files successfully uploaded!'); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /controllers/BrowseController.php: -------------------------------------------------------------------------------- 1 | getCurrentFolder(); 29 | if (!$currentFolder->content->canView()) { 30 | throw new HttpException(403); 31 | } 32 | 33 | return $this->render('index', [ 34 | 'contentContainer' => $this->contentContainer, 35 | 'folder' => $currentFolder, 36 | 'canWrite' => $this->canWrite(), 37 | ]); 38 | } 39 | 40 | public function actionFileList() 41 | { 42 | return $this->asJson(['output' => $this->renderFileList()]); 43 | } 44 | 45 | /** 46 | * Returns rendered file list. 47 | * 48 | * @return string 49 | */ 50 | public function renderFileList(): string 51 | { 52 | return FileList::widget([ 53 | 'folder' => $this->getCurrentFolder(), 54 | 'contentContainer' => $this->contentContainer, 55 | ]); 56 | } 57 | 58 | public function actionLoadEntry() 59 | { 60 | if ($file = $this->getFileById()) { 61 | return $this->asJson([ 62 | 'output' => $this->renderFileRow($file), 63 | // Additional scripts may be generated here in order to display some messages in info footer bar 64 | 'scripts' => $this->renderAjaxContent(''), 65 | ]); 66 | } 67 | 68 | return $this->asJson([ 69 | 'success' => false, 70 | 'error' => Yii::t('CfilesModule.base', 'No file found!'), 71 | ]); 72 | } 73 | 74 | private function getFileById(): ?File 75 | { 76 | $fileId = Yii::$app->request->get('id'); 77 | $fileId = str_starts_with((string) $fileId, 'file_') ? substr((string) $fileId, 5) : 0; 78 | 79 | if (empty($fileId)) { 80 | return null; 81 | } 82 | 83 | return File::find()->readable()->where(['cfiles_file.id' => $fileId])->one(); 84 | } 85 | 86 | private function renderFileRow(File $file) 87 | { 88 | if ($file->parent_folder_id) { 89 | $folder = Folder::findOne(['id' => $file->parent_folder_id]); 90 | } else { 91 | $folder = $this->getRootFolder(); 92 | } 93 | 94 | return FileSystemItem::widget([ 95 | 'folder' => $folder, 96 | 'row' => new FileRow(['item' => $file]), 97 | ]); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /controllers/UploadController.php: -------------------------------------------------------------------------------- 1 | [WriteAccess::class]], 35 | ]; 36 | } 37 | 38 | public function actions() 39 | { 40 | return [ 41 | 'index' => ['class' => UploadAction::class], 42 | ]; 43 | } 44 | 45 | public function actionImport($fid) 46 | { 47 | $guids = Yii::$app->request->post('guids'); 48 | $guids = is_string($guids) ? array_map(trim(...), explode(',', $guids)) : $guids; 49 | 50 | if (!is_array($guids)) { 51 | throw new HttpException(400); 52 | } 53 | 54 | $folder = Folder::findOne($fid); 55 | 56 | $errors = []; 57 | 58 | foreach ($guids as $guid) { 59 | $cFile = ModelFile::findOne(['guid' => $guid]); 60 | 61 | if (!$cFile) { 62 | $error = Yii::t('CfilesModule.base', 'Could not import file with guid {guid}. File not found', ['guid' => $guid]); 63 | $errors[] = $error; 64 | Yii::error($error); 65 | continue; 66 | } 67 | 68 | $cFile->show_in_stream = false; 69 | 70 | $file = new File($this->contentContainer); 71 | $file->setFileContent($cFile); 72 | $folder->moveItem($file); 73 | 74 | $file->visibility = Content::VISIBILITY_PRIVATE; 75 | $file->save(); 76 | 77 | if ($file->hasErrors()) { 78 | $errors[] = $this->actionResponseError($file); 79 | } 80 | 81 | if ($file->baseFile->hasErrors()) { 82 | $errors[] = $this->actionResponseError($file->baseFile); 83 | } 84 | } 85 | 86 | if ($errors) { 87 | array_unshift($errors, Yii::t('CfilesModule.base', 'Some files could not be imported: ')); 88 | } 89 | 90 | return $this->asJson(['success' => empty($errors), 'errors' => $errors]); 91 | } 92 | 93 | public function actionResponseError(ActiveRecord $record) 94 | { 95 | $errorMsg = Yii::t('CfilesModule.base', 'Some files could not be imported: '); 96 | foreach ($record->getErrors() as $errors) { 97 | foreach ($errors as $error) { 98 | $errorMsg .= $error . ' '; 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /widgets/VersionsView.php: -------------------------------------------------------------------------------- 1 | initVersions(); 53 | } 54 | 55 | /** 56 | * Initialise versions 57 | */ 58 | private function initVersions() 59 | { 60 | $pagination = new Pagination([ 61 | 'page' => $this->page - 1, 62 | 'pageSize' => $this->pageSize, 63 | 'totalCount' => $this->file->baseFile->getHistoryFiles()->count(), 64 | ]); 65 | 66 | $this->isLastPage = ($pagination->page >= $pagination->pageCount - 1); 67 | 68 | $this->versions = $this->file->baseFile 69 | ->getHistoryFiles() 70 | ->offset($pagination->offset) 71 | ->limit($pagination->limit) 72 | ->all(); 73 | } 74 | 75 | /** 76 | * @inheritdoc 77 | */ 78 | public function run() 79 | { 80 | return $this->render('versionsView', [ 81 | 'versionsRowsHtml' => $this->renderVersions(), 82 | 'nextPageVersionsUrl' => $this->getNextPageVersionsUrl(), 83 | ]); 84 | } 85 | 86 | public function renderVersions(): string 87 | { 88 | $html = ''; 89 | 90 | if ($this->page == 1) { 91 | $html .= VersionItem::widget([ 92 | 'version' => $this->file->baseFile, 93 | 'revertUrl' => false, 94 | 'downloadUrl' => $this->file->baseFile->getUrl(), 95 | 'deleteUrl' => false, 96 | ]); 97 | } 98 | 99 | foreach ($this->versions as $version) { 100 | $html .= VersionItem::widget([ 101 | 'version' => $version, 102 | 'revertUrl' => $this->file->getVersionsUrl($version->id), 103 | 'downloadUrl' => $version->getFileUrl(), 104 | 'deleteUrl' => $this->file->getDeleteVersionUrl($version->id), 105 | ]); 106 | } 107 | 108 | return $html; 109 | } 110 | 111 | public function isLastPage(): bool 112 | { 113 | return $this->isLastPage; 114 | } 115 | 116 | private function getNextPageVersionsUrl(): string 117 | { 118 | return $this->isLastPage() ? '' 119 | : $this->file->content->container->createUrl('/cfiles/version/page', ['id' => $this->file->id]); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /tests/codeception/unit/MigrateFromOldTest.php: -------------------------------------------------------------------------------- 1 | becomeUser('Admin'); 28 | $space1 = Space::findOne(1); 29 | 30 | $this->assertTrue($this->createFile($space1, 'f1.txt')); 31 | $this->assertTrue($this->createFile($space1, 'f2.txt')); 32 | $this->assertTrue($this->createFile($space1, 'f3.txt')); 33 | $this->assertTrue($this->createFile($space1, 'f4.txt')); 34 | $this->assertTrue($this->createFile($space1, 'f5.txt')); 35 | 36 | // Add one folder manually with a file 37 | $folder = new Folder($space1); 38 | $folder->title = 'fo1'; 39 | $this->assertTrue($folder->save(false)); 40 | 41 | $fileA = new File($space1); 42 | $fileA->setUploadedFile(new UploadedFile([ 43 | 'name' => 'fileA.txt', 44 | 'size' => 1024, 45 | 'type' => 'text/plain', 46 | ])); 47 | 48 | $folder->moveItem($fileA); 49 | 50 | $this->assertTrue($this->createFolder($space1, 'fo2')); 51 | $this->assertTrue($this->createFolder($space1, 'fo3')); 52 | $this->assertTrue($this->createFolder($space1, 'fo4')); 53 | $this->assertTrue($this->createFolder($space1, 'fo5')); 54 | 55 | $root = Folder::initRoot($space1); 56 | $root->migrateFromOldStructure(); 57 | 58 | $children = $root->getChildren(); 59 | 60 | $this->assertEquals(10, count($children)); 61 | $this->assertNotNull($root->findFileByName('f1.txt')); 62 | $this->assertNotNull($root->findFileByName('f2.txt')); 63 | $this->assertNotNull($root->findFileByName('f3.txt')); 64 | $this->assertNotNull($root->findFileByName('f4.txt')); 65 | $this->assertNotNull($root->findFileByName('f5.txt')); 66 | 67 | $this->assertNotNull($root->findFolderByName('fo1')); 68 | $this->assertNotNull($root->findFolderByName('fo2')); 69 | $this->assertNotNull($root->findFolderByName('fo3')); 70 | $this->assertNotNull($root->findFolderByName('fo4')); 71 | $this->assertNotNull($root->findFolderByName('fo5')); 72 | 73 | $root->refresh(); 74 | 75 | $this->assertNull($root->parent_folder_id); 76 | 77 | } 78 | 79 | public function createFile($space, $name) 80 | { 81 | $file = new File($space); 82 | $file->setUploadedFile(new UploadedFile([ 83 | 'name' => $name, 84 | 'size' => 1024, 85 | 'type' => 'text/plain', 86 | ])); 87 | 88 | //Save without parent_id; 89 | return $file->save(false); 90 | } 91 | 92 | public function createFolder($space, $title) 93 | { 94 | $folder = new Folder($space); 95 | $folder->title = $title; 96 | return $folder->save(false); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /tests/codeception/acceptance/VisibilityCest.php: -------------------------------------------------------------------------------- 1 | amAdmin(); 19 | $I->wantToTest('the visibility of folders'); 20 | $I->amGoingTo('install the cfiles module for space 1'); 21 | $I->enableCfilesOnSpace(); 22 | 23 | $I->createFolder('visibility', 'visibility test'); 24 | $I->expect('The new folder to be private'); 25 | $I->seeElement('.folder-visibility .fa-lock'); 26 | 27 | $I->uploadFile(); 28 | $I->expect('The new file to be private'); 29 | $I->seeElement('[data-cfiles-item] .fa-lock'); 30 | 31 | $I->click('Add directory', '.files-action-menu'); 32 | 33 | // Create another folder 34 | $I->waitForText('Create folder', 10, '#globalModal'); 35 | $I->fillField('Folder[title]', 'visibility2'); 36 | $I->fillField('Folder[description]', 'visibility2'); 37 | 38 | $I->expect('The folder visibility to be private'); 39 | $I->waitForElement('input#folder-visibility:disabled'); 40 | 41 | $I->click('Save', '#globalModal'); 42 | $I->waitForText('This folder is empty.'); 43 | $I->seeElement('.folder-visibility .fa-lock'); 44 | $I->uploadFile(); 45 | $I->expect('The new file to be private'); 46 | $I->seeElement('[data-cfiles-item] .fa-lock'); 47 | 48 | $I->amUser1(true); 49 | $I->amOnSpace(1, '/cfiles/browse'); 50 | $I->expect('Not to see the files entry since there are no public files available'); 51 | $I->see('Files from the stream', '#fileList'); 52 | 53 | $I->amAdmin(true); 54 | $I->amOnSpace(1, '/cfiles/browse'); 55 | 56 | $I->amGoingTo('set the folder visibility to public'); 57 | $I->clickFolderContext(3, 'Edit'); 58 | $I->waitForText('Edit folder', 10, '#globalModal'); 59 | $I->jsClick('input#folder-visibility'); 60 | $I->click('Save', '#globalModal'); 61 | $I->waitForText('visibility2', 10, '#fileList'); 62 | 63 | $I->expect('all subfiles and subfolders to be public too'); 64 | $I->seeElement('.folder-visibility .fa-unlock'); 65 | $I->seeElement('[data-cfiles-item="folder_4"] .fa-unlock'); 66 | $I->seeElement('[data-cfiles-item="file_1"] .fa-unlock'); 67 | 68 | $I->click('visibility2', '#fileList'); 69 | $I->waitForText('visibility2', 10, '#cfiles-crumb'); 70 | $I->seeElement('[data-cfiles-item="file_2"] .fa-unlock'); 71 | 72 | $I->amGoingTo('Reset the file visibility of /visibility/visibility2/test.txt to private'); 73 | $I->clickFileContext(2, 'Edit'); 74 | $I->waitForText('Edit file', 10, '#globalModal'); 75 | $I->jsClick('input[type="checkbox"][name="File[visibility]"]'); 76 | $I->click('Save', '#globalModal'); 77 | 78 | $I->seeSuccess(); 79 | $I->waitForElementVisible('[data-cfiles-item="file_2"] .fa-lock'); 80 | 81 | $I->amUser1(true); 82 | $I->amOnSpace(1, '/cfiles/browse'); 83 | $I->seeElement('#fileList'); 84 | $I->see('visibility'); 85 | $I->click('visibility', '#fileList'); 86 | $I->waitForText('visibility2', 10, '#fileList'); 87 | $I->see('test.txt', '#fileList'); 88 | $I->click('visibility2', '#fileList'); 89 | $I->waitForText('visibility2', 10, '#cfiles-crumb'); 90 | $I->dontSee('test.txt', '#fileList'); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /widgets/views/fileSelectionMenu.php: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 |
    22 |
    23 | 26 | 85 |
    86 |
    87 | -------------------------------------------------------------------------------- /widgets/views/fileListMenu.php: -------------------------------------------------------------------------------- 1 | createUrl('/cfiles/zip/download', ['fid' => $folder->id]); 20 | $zipUploadUrl = $contentContainer->createUrl('/cfiles/zip/upload', ['fid' => $folder->id]); 21 | 22 | $addFolderUrl = $contentContainer->createUrl('/cfiles/edit/folder', ['fid' => $folder->id]); 23 | $editFolderUrl = $contentContainer->createUrl('/cfiles/edit/folder', ['id' => $folder->getItemId()]); 24 | 25 | $uploadUrl = $contentContainer->createUrl('/cfiles/upload', ['fid' => $folder->id]); 26 | ?> 27 | 28 |
    29 | $folder, 31 | 'contentContainer' => $contentContainer, 32 | ]);?> 33 | 34 | parentFolder) : ?> 35 | parentFolder->getUrl())->left()->setText(''); ?> 36 | 37 | 38 | 39 | isAllPostedFiles()): ?> 40 |
    41 | 42 | 43 | 44 |
    45 | 'd-none d-sm-inline'])) 46 | ->load($addFolderUrl) 47 | ->icon('fa-folder') ?> 48 | isRoot()): ?> 49 | 52 | 60 | 61 |
    62 | 63 | 64 | 65 | 66 | 'cfilesUploadFiles', 68 | 'progress' => '#cfiles_progress', 69 | 'url' => $uploadUrl, 70 | 'preview' => '#cfiles-folderView', 71 | 'tooltip' => false, 72 | 'cssButtonClass' => 'btn-accent', 73 | 'label' => Html::tag('span', Yii::t('CfilesModule.base', 'Add file(s)'), ['class' => 'd-none d-sm-inline']), 74 | 'dropZone' => '#cfiles-container', 75 | 'pasteZone' => 'body', 76 | ]) ?> 77 | $uploadButton, 'handlers' => $fileHandlers, 'cssButtonClass' => 'btn-accent', 'pullRight' => true]); ?> 78 | 79 | 'cfilesUploadZipFile', 81 | 'progress' => '#cfiles_progress', 82 | 'url' => $zipUploadUrl, 83 | 'preview' => '#cfiles-folderView', 84 | ]) ?> 85 | 86 |
    87 | 88 |
    89 | -------------------------------------------------------------------------------- /models/forms/MoveForm.php: -------------------------------------------------------------------------------- 1 | contentContainer = $this->root->content->container; 50 | parent::init(); 51 | } 52 | 53 | /** 54 | * @inheritdoc 55 | */ 56 | public function rules() 57 | { 58 | return [ 59 | ['destId', 'required'], 60 | ['destId', 'integer'], 61 | ['destId', 'validateDestination'], 62 | ]; 63 | } 64 | 65 | /** 66 | * @param $model MoveForm 67 | * @param $attribute 68 | */ 69 | public function validateDestination($attribute) 70 | { 71 | $this->destination = Folder::findOne($this->destId); 72 | 73 | if (!$this->destination) { 74 | $this->addError($attribute, Yii::t('CfilesModule.base', 'Destination folder not found!')); 75 | return; 76 | } 77 | 78 | if ($this->sourceFolder->id == $this->destination->id) { 79 | $this->addError($attribute, Yii::t('CfilesModule.base', 'Moving to the same folder is not valid.')); 80 | return; 81 | } 82 | 83 | if ($this->destination->isAllPostedFiles() || $this->destination->content->container->id !== $this->contentContainer->id) { 84 | $this->addError($attribute, Yii::t('CfilesModule.base', 'Moving to this folder is invalid.')); 85 | return; 86 | } 87 | } 88 | 89 | /** 90 | * @return string move action url 91 | */ 92 | public function getMoveUrl() 93 | { 94 | return $this->sourceFolder->createUrl('/cfiles/move'); 95 | } 96 | 97 | /** 98 | * @return string URL to move Content to another Container 99 | */ 100 | public function getMoveToContainerUrl(): ?string 101 | { 102 | if (count($this->selection) !== 1) { 103 | // Only single selected File/Folder can be moved to another Container 104 | return null; 105 | } 106 | 107 | $item = FileSystemItem::getItemById($this->selection[0]); 108 | if (!$item || !$item->content->container || !$item->content->checkMovePermission()) { 109 | return null; 110 | } 111 | 112 | return $item->content->container->createUrl('/content/move/move', ['id' => $item->content->id]); 113 | } 114 | 115 | /** 116 | * Executes the actual move of the selection files from source into target. 117 | * @return bool 118 | */ 119 | public function save() 120 | { 121 | if (!$this->validate()) { 122 | return false; 123 | } 124 | 125 | $result = true; 126 | 127 | foreach ($this->selection as $selectedItemId) { 128 | $item = FileSystemItem::getItemById($selectedItemId); 129 | 130 | if (!$this->destination->moveItem($item)) { 131 | $this->addItemErrors($item); 132 | $result = false; 133 | } 134 | } 135 | 136 | return $result; 137 | } 138 | 139 | /** 140 | * @param FileSystemItem $item 141 | */ 142 | public function addItemErrors(FileSystemItem $item) 143 | { 144 | foreach ($item->errors as $key => $error) { 145 | $this->addErrors([$key => $error]); 146 | } 147 | } 148 | 149 | 150 | } 151 | -------------------------------------------------------------------------------- /resources/css/directorylist.css: -------------------------------------------------------------------------------- 1 | .directory-list * { 2 | font-family: sans-serif; 3 | font-size: 12px; 4 | line-height: 1.5em; 5 | } 6 | 7 | .directory-list { 8 | border: 1px solid #808080; 9 | color: #333333; 10 | padding: 4px; 11 | overflow: auto; 12 | width: auto; 13 | height: 320px; 14 | margin-bottom: 1.5em; 15 | } 16 | 17 | .directory-list ul,.directory-list li { 18 | padding: 0; 19 | margin: 0; 20 | display: block; 21 | background: none; 22 | list-style: none; 23 | } 24 | 25 | .directory-list ul { 26 | background: url(../directorylist/directory_last.png) no-repeat 0 -8px; 27 | } 28 | 29 | .directory-list ul ul { 30 | padding-left: 16px; 31 | background: url(../directorylist/directory.png) repeat-y 0 0; 32 | } 33 | 34 | .directory-list li:last-child>ul { 35 | background: none; 36 | } 37 | 38 | .directory-list li.last-child>ul { 39 | background: none; 40 | } 41 | 42 | .directory-list li { 43 | background: url(../directorylist/directory_join.png) no-repeat 0 -8px; 44 | } 45 | 46 | .directory-list li.last-child { 47 | background-image: url(../directorylist/directory_last-child.png); 48 | } 49 | 50 | .directory-list li:last-child { 51 | background-image: url(../directorylist/directory_last-child.png); 52 | } 53 | 54 | .directory-list li.hassub { 55 | background-image: url(../directorylist/directory_join_hassub.png); 56 | } 57 | 58 | .directory-list li.hassub.expand { 59 | background-image: url(../directorylist/directory_join_hassub_expand.png); 60 | } 61 | 62 | .directory-list li.last-child.hassub { 63 | background-image: url(../directorylist/directory_last-child_hassub.png); 64 | } 65 | 66 | .directory-list li:last-child.hassub { 67 | background-image: url(../directorylist/directory_last-child_hassub.png); 68 | } 69 | 70 | .directory-list li.last-child.hassub.expand { 71 | background-image: 72 | url(../directorylist/directory_last-child_hassub_expand.png); 73 | } 74 | 75 | .directory-list li:last-child.hassub.expand { 76 | background-image: 77 | url(../directorylist/directory_last-child_hassub_expand.png); 78 | } 79 | 80 | * html .directory-list ul { 81 | background-image: none !important; 82 | } 83 | 84 | * html .directory-list li { 85 | background-image: none !important; 86 | } 87 | 88 | * html .directory-list li.hassub { 89 | background-image: url(../directorylist/directory_hassub.png) !important; 90 | background-position: 3px 0; 91 | } 92 | 93 | * html .directory-list li.expand { 94 | background-image: url(../directorylist/directory_exand.png) !important; 95 | background-position: 3px 0; 96 | } 97 | 98 | .directory-list div,.directory-list span,.directory-list a { 99 | display: inline-block; 100 | height: 1.5em; 101 | padding: 0 4px 0 39px; 102 | margin: 0; 103 | background: url(../directorylist/folder.png) no-repeat 16px 0; 104 | text-decoration: none; 105 | font-weight: normal; 106 | white-space: nowrap; 107 | } 108 | 109 | .directory-list div { 110 | color: #000000; 111 | padding-left: 23px; 112 | background-position: 3px 0; 113 | background-image: url(../directorylist/drive.png); 114 | } 115 | 116 | .directory-list li>span,.directory-list>div { 117 | cursor: pointer; 118 | } 119 | 120 | .directory-list .selectable.selectedFolder { 121 | background-color: #EBEBEB !important; 122 | border-left: 3px solid #6FDBE8 !important; 123 | } 124 | 125 | .directory-list a { 126 | background-image: url(../directorylist/page_white.png); 127 | } 128 | 129 | .directory-list a { 130 | background-image: url(../directorylist/page_white.png); 131 | } 132 | 133 | .directory-list a[type^='image/'] { 134 | background-image: url(../directorylist/page_white_picture.png); 135 | } 136 | 137 | .directory-list a[type^='text/'] { 138 | background-image: url(../directorylist/page_white_text.png); 139 | } 140 | 141 | .directory-list a[type='text/html'] { 142 | background-image: url(../directorylist/page_white_code.png); 143 | } 144 | 145 | .directory-list a[type='application/zip'],.directory-list a[type='application/x-gzip'] 146 | { 147 | background-image: url(../directorylist/page_white_compressed.png); 148 | } 149 | 150 | .directory-list a[type='application/x-httpd-php'] { 151 | background-image: url(../directorylist/page_white_php.png); 152 | } -------------------------------------------------------------------------------- /widgets/views/fileList.php: -------------------------------------------------------------------------------- 1 | 21 | 22 |
    23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | getModule('cfiles')->settings->get('displayDownloadCount')): ?> 41 | 42 | 43 | 44 | isAllPostedFiles()): // Files currently have no content object but the Post they may be connected to. ?> 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | $folder, 56 | 'row' => $row, 57 | 'itemsSelectable' => $itemsSelectable 58 | ]) ?> 59 | 60 | 61 |
    28 | 'allselect']); ?> 29 | > 33 | 34 | >>>
    62 | 63 |
    64 | $pagination]); ?> 65 |
    66 | 67 |
    68 | 69 |
    70 |
    71 |
    72 |
    73 |

    74 | 75 |

    76 | isAllPostedFiles()): ?> 77 | 78 | 79 | 80 | 81 | 82 | 83 |
    84 |
    85 |
    86 | 87 | -------------------------------------------------------------------------------- /controllers/rest/FolderController.php: -------------------------------------------------------------------------------- 1 | $containerId]); 43 | if ($containerRecord === null) { 44 | return $this->returnError(404, 'Content container not found!'); 45 | } 46 | /** @var ContentContainerActiveRecord $container */ 47 | $container = $containerRecord->getPolymorphicRelation(); 48 | $params = Yii::$app->request->getBodyParams(); 49 | $isPublicDirectory = isset($params['Folder']['visibility']) && $params['Folder']['visibility'] === Content::VISIBILITY_PUBLIC; 50 | 51 | if (empty($params['target_id'])) { 52 | if (!($targetDir = Folder::getRoot($container)) 53 | && !($targetDir = Folder::initRoot($container))) { 54 | return $this->returnError(400, 'Target folder id is required!'); 55 | } 56 | } else { 57 | $targetDir = Folder::findOne(['id' => $params['target_id']]); 58 | } 59 | 60 | if (empty($targetDir)) { 61 | return $this->returnError(404, 'cFiles folder not found!'); 62 | } 63 | if (!$container->can(ManageFiles::class)) { 64 | return $this->returnError(403, 'You cannot create folder in this container!'); 65 | } 66 | 67 | if ((! $targetDir->isRoot() && $targetDir->content->isPrivate()) && $isPublicDirectory) { 68 | return $this->returnError(403, 'Could not create public directory inside private directory!'); 69 | } 70 | 71 | $folder = $targetDir->newFolder(); 72 | $folder->content->container = $container; 73 | 74 | if ($folder->load($params) && $folder->save()) { 75 | return RestDefinitions::getFolder(Folder::findOne(['id' => $folder->id])); 76 | } 77 | 78 | if ($folder->hasErrors()) { 79 | return $this->returnError(422, 'Validation failed', [ 80 | 'errors' => $folder->getErrors(), 81 | ]); 82 | } else { 83 | Yii::error('Could not create valid folder.', 'api'); 84 | return $this->returnError(500, 'Internal error while create folder!'); 85 | } 86 | } 87 | 88 | public function actionUpdate($id) 89 | { 90 | $folder = Folder::findOne(['id' => $id]); 91 | 92 | if ($folder === null) { 93 | return $this->returnError(404, 'cFiles folder not found!'); 94 | } 95 | if (!$folder->content->canEdit()) { 96 | return $this->returnError(403, 'You cannot edit this folder!'); 97 | } 98 | 99 | if ($folder->load(Yii::$app->request->getBodyParams()) && $folder->save()) { 100 | return RestDefinitions::getFolder(Folder::findOne(['id' => $folder->id])); 101 | } 102 | 103 | if ($folder->hasErrors()) { 104 | return $this->returnError(422, 'Validation failed', [ 105 | 'errors' => $folder->getErrors(), 106 | ]); 107 | } else { 108 | Yii::error('Could not update valid folder.', 'api'); 109 | return $this->returnError(500, 'Internal error while update cFiles folder!'); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /controllers/VersionController.php: -------------------------------------------------------------------------------- 1 | getFile(); 32 | 33 | if (!$file->content->canEdit()) { 34 | throw new HttpException(403); 35 | } 36 | 37 | $model = new VersionForm(['file' => $file]); 38 | 39 | if (!$model->load()) { 40 | return $this->renderAjax('index', [ 41 | 'model' => $model, 42 | ]); 43 | } 44 | 45 | if ($model->save()) { 46 | $this->view->success(Yii::t('CfilesModule.base', 'File {fileName} has been reverted to version from {fileDateTime}', [ 47 | 'fileName' => $model->file->baseFile->file_name, 48 | 'fileDateTime' => Yii::$app->formatter->asDatetime($model->getFileVersion()->created_at, 'short'), 49 | ])); 50 | } else { 51 | $errorMsg = ''; 52 | foreach ($model->getErrors() as $errors) { 53 | foreach ($errors as $error) { 54 | $errorMsg .= $error . ' '; 55 | } 56 | } 57 | $this->view->error($errorMsg); 58 | } 59 | 60 | return $this->htmlRedirect($model->file->content->container->createUrl('/cfiles/browse', ['fid' => $model->file->parent_folder_id])); 61 | } 62 | 63 | /** 64 | * Load file versions for the single requested page 65 | */ 66 | public function actionPage() 67 | { 68 | $file = $this->getFile(); 69 | 70 | if (!$file->content->canEdit()) { 71 | throw new HttpException(403); 72 | } 73 | 74 | $versionsView = new VersionsView([ 75 | 'file' => $file, 76 | 'page' => (int)Yii::$app->request->get('page', 2), 77 | ]); 78 | 79 | return $this->asJson([ 80 | 'html' => $versionsView->renderVersions(), 81 | 'isLast' => $versionsView->isLastPage(), 82 | ]); 83 | } 84 | 85 | /** 86 | * Action to delete a version of the requested File 87 | * @throws HttpException 88 | */ 89 | public function actionDelete() 90 | { 91 | $this->forcePostRequest(); 92 | 93 | $file = $this->getFile(); 94 | 95 | if (!$file->canEdit()) { 96 | throw new HttpException(403); 97 | } 98 | 99 | $fileVersionId = (int)Yii::$app->request->get('version'); 100 | $deletedFileVersion = $file->baseFile->getFileHistoryById($fileVersionId); 101 | 102 | if (!$deletedFileVersion) { 103 | throw new HttpException(404, 'Version not found!'); 104 | } 105 | 106 | $deletedVersionDate = Yii::$app->formatter->asDatetime($deletedFileVersion->created_at, 'short'); 107 | 108 | if (!$deletedFileVersion->delete()) { 109 | return $this->asJson([ 110 | 'error' => Yii::t('CfilesModule.base', 'The version "{versionDate}" could not be deleted!', ['versionDate' => $deletedVersionDate]), 111 | ]); 112 | } 113 | 114 | return $this->asJson([ 115 | 'deleted' => $fileVersionId, 116 | 'message' => Yii::t('CfilesModule.base', 'The version "{versionDate}" has been deleted.', ['versionDate' => $deletedVersionDate]), 117 | ]); 118 | } 119 | 120 | private function getFile(): File 121 | { 122 | /* @var File $file */ 123 | $file = File::find() 124 | ->readable() 125 | ->andWhere(['cfiles_file.id' => Yii::$app->request->get('id')]) 126 | ->one(); 127 | 128 | if (!$file) { 129 | throw new HttpException(404, 'File not found!'); 130 | } 131 | 132 | return $file; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /libs/ZipExtractor.php: -------------------------------------------------------------------------------- 1 | cleanup(); 32 | if (strtolower($file->extension) === 'zip') { 33 | $sourcePath = $this->getZipOutputPath() . '/zipped.zip'; 34 | $extractionPath = $this->getZipOutputPath() . '/extracted'; 35 | 36 | if ($file->saveAs($sourcePath, false)) { 37 | $this->unpackArchive($sourcePath, $extractionPath); 38 | $this->generateModelsFromFilesystem($targetFolder, $extractionPath); 39 | } else { 40 | $targetFolder->addError('upload', Yii::t('CfilesModule.base', 'Archive %filename% could not be extracted.', ['%filename%' => $file->name])); 41 | } 42 | } else { 43 | $targetFolder->addError('upload', Yii::t('CfilesModule.base', '%filename% has invalid extension and was skipped.', ['%filename%' => $file->name])); 44 | } 45 | 46 | return $targetFolder; 47 | } 48 | 49 | /** 50 | * Unzips an archive from sourcePath to extractionPath. 51 | * 52 | * @param string $sourcePath 53 | * @param string $extractionPath 54 | */ 55 | protected function unpackArchive($sourcePath, $extractionPath) 56 | { 57 | $zip = new \ZipArchive(); 58 | $zip->open($sourcePath); 59 | $zip->extractTo($extractionPath); 60 | $zip->close(); 61 | } 62 | 63 | /** 64 | * Generate the cfolder and cfile structure in the database of a given folder and all its subfolders recursively. 65 | * 66 | * @param Folder $targetFolder the folders parent folder. 67 | * @param string $folderPath the path of the folder. 68 | * @param Folder|null $root the $root folder which is used for adding errors if not given $targetFolder will be used as $root 69 | */ 70 | protected function generateModelsFromFilesystem(Folder $targetFolder, $folderPath, ?Folder $root = null) 71 | { 72 | 73 | // remove unwanted parent folder references from the scanned files 74 | $files = array_diff(scandir($folderPath), ['..','.']); 75 | 76 | if (!$root) { 77 | $root = $targetFolder; 78 | } 79 | 80 | foreach ($files as $file) { 81 | $filePath = $folderPath . DIRECTORY_SEPARATOR . $file; 82 | if (is_dir($filePath)) { 83 | $folder = $targetFolder->findFolderByName($file); 84 | 85 | if (!$folder) { 86 | $folder = $targetFolder->newFolder($file); 87 | if (!$folder->save()) { 88 | $root->addError('upload', Yii::t('CfilesModule.base', 'An error occurred while creating folder {folder}.', ['folder' => $file])); 89 | continue; 90 | } 91 | } 92 | 93 | $this->generateModelsFromFilesystem($folder, $filePath, $root); 94 | } else { 95 | $result = $this->generateModelFromFile($targetFolder, $folderPath, $file); 96 | if ($result->hasErrors()) { 97 | $root->addError('upload', Yii::t('CfilesModule.base', 'An error occurred while unpacking {filename}.', ['filename' => $file])); 98 | } 99 | } 100 | } 101 | } 102 | 103 | /** 104 | * Create a cfile model and create and connect it with its basefile File model from a given data file, connect it with its parent folder. 105 | * TODO: This method has a lot in common with BrowseController/actionUpload, common logic needs to be extracted and reused 106 | * 107 | * @param Folder $targetFolder the files pid. 108 | * @param string $folderPath the path of the folder the file data lies in. 109 | * @param string $filename the files name. 110 | */ 111 | protected function generateModelFromFile(Folder $targetFolder, $folderPath, $filename) 112 | { 113 | $filePath = $folderPath . DIRECTORY_SEPARATOR . $filename; 114 | return $targetFolder->addFileFromPath($filename, $filePath); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /resources/css/cfiles.css: -------------------------------------------------------------------------------- 1 | #cfiles-folderView a:not(:hover) { 2 | color: var(--hh-text-color-highlight); 3 | } 4 | 5 | .overflow-ellipsis { 6 | position: relative; 7 | overflow: hidden; 8 | max-width: 100%; 9 | text-overflow: ellipsis; 10 | white-space: nowrap; 11 | } 12 | .cfiles-content .breadcrumb { 13 | border-radius: 0; 14 | margin: 0; 15 | } 16 | 17 | .cfiles-content .file-controls { 18 | font-size: 11px; 19 | color: var(--hh-text-color-soft2); 20 | margin-top: 10px; 21 | margin-bottom: 0; 22 | } 23 | .cfiles-content .file-controls a { 24 | font-size: 11px; 25 | color: var(--hh-text-color-soft2); 26 | margin-top: 10px; 27 | margin-bottom: 0; 28 | } 29 | .fileinput-button input { 30 | position: absolute; 31 | top: 0; 32 | right: 0; 33 | margin: 0; 34 | opacity: 0; 35 | -ms-filter: 'alpha(opacity=0)'; 36 | font-size: 200px; 37 | direction: ltr; 38 | cursor: pointer; 39 | } 40 | 41 | .panel .panel-body .files-action-menu .btn { 42 | font-size: 1em !important; 43 | font-weight: normal !important; 44 | } 45 | .panel .panel-body .files-action-menu .btn-group .split-toggle { 46 | width: auto !important; 47 | float: right !important; 48 | border-radius: 0px 3px 3px 0px; 49 | } 50 | .panel .panel-body .files-action-menu .btn-group .split-toggle:not(:first-child):not(:last-child):not(.dropdown-toggle) { 51 | border-radius: 0px 3px 3px 0px; 52 | } 53 | .panel .panel-body .files-action-menu .btn-group > .split-button { 54 | width: auto !important; 55 | overflow: hidden; 56 | display: block; 57 | float: none !important; 58 | border-radius: 3px 0px 0px 3px; 59 | } 60 | 61 | .filedelete-button { 62 | cursor: pointer; 63 | } 64 | .filemove-button { 65 | cursor: pointer; 66 | } 67 | .downloadzipped-button { 68 | cursor: pointer; 69 | } 70 | .table-responsive td .title, 71 | .table-responsive td .timestamp, 72 | .table-responsive td .creator, 73 | .table-responsive td .size { 74 | overflow: hidden; 75 | -ms-text-overflow: ellipsis; 76 | -o-text-overflow: ellipsis; 77 | text-overflow: ellipsis; 78 | display: block; 79 | white-space: nowrap; 80 | } 81 | .table-responsive > table > tbody > tr > td { 82 | vertical-align: middle; 83 | } 84 | .table-responsive .table .file-controls { 85 | font-size: 11px; 86 | margin-top: 0; 87 | margin-bottom: 0; 88 | } 89 | .table-responsive .table .file-controls a { 90 | font-size: 11px; 91 | margin-top: 0; 92 | margin-bottom: 0; 93 | } 94 | .table-responsive .table .file-controls hr { 95 | margin-top: 2px; 96 | margin-bottom: 2px; 97 | } 98 | 99 | #fileList .table-responsive { 100 | overflow: initial; 101 | border: none; 102 | } 103 | 104 | #bs-table.table .file-actions { 105 | padding-left: 0; 106 | padding-right: 2px; 107 | width: 0; 108 | } 109 | 110 | @media (min-width: 418px) { 111 | #bs-table.table tr .context-icon { 112 | display: none; 113 | } 114 | #bs-table.table tr:hover .context-icon { 115 | display: inline-block; 116 | } 117 | } 118 | 119 | #bs-table.table>thead>tr>th, 120 | #bs-table.table>tbody>tr>th, 121 | #bs-table.table>tfoot>tr>th, 122 | #bs-table.table>thead>tr>td, 123 | #bs-table.table>tbody>tr>td, 124 | #bs-table.table>tfoot>tr>td { 125 | padding: 10px 5px 5px 5px; 126 | height: 45px; 127 | } 128 | 129 | #bs-table.table>thead>tr>th label, 130 | #bs-table.table>tbody>tr>th label, 131 | #bs-table.table>tfoot>tr>th label, 132 | #bs-table.table>thead>tr>td label, 133 | #bs-table.table>tbody>tr>td label, 134 | #bs-table.table>tfoot>tr>td label { 135 | margin-bottom: 0px; 136 | } 137 | 138 | #bs-table.table ul.contextMenu li a { 139 | font-size: 14px; 140 | font-weight: normal; 141 | } 142 | 143 | #bs-table.table tr.ui-draggable-dragging { 144 | z-index: 10000; 145 | } 146 | 147 | #fileList [data-ui-sort] { 148 | font-weight:bold; 149 | cursor: pointer; 150 | } 151 | 152 | #fileList [data-ui-order="ASC"]:after { 153 | content: "\25bc"; 154 | } 155 | 156 | #fileList [data-ui-order="DESC"]:after { 157 | content: "\25b2"; 158 | } 159 | 160 | 161 | .table-responsive td .title { 162 | position: absolute; 163 | width: 100%; 164 | top: -10px; 165 | } 166 | @media (min-width: 420px) { 167 | .table-responsive td:nth-child(2) { 168 | width: 25%; 169 | max-width: 25%; 170 | } 171 | } 172 | @media (min-width: 768px) { 173 | .table-responsive td:nth-child(2) { 174 | width: 30%; 175 | max-width: 30%; 176 | } 177 | } 178 | @media (min-width: 992px) { 179 | .table-responsive td:nth-child(2) { 180 | width: 35%; 181 | max-width: 35%; 182 | } 183 | } 184 | @media (min-width: 1200px) { 185 | .table-responsive td:nth-child(2) { 186 | width: 40%; 187 | max-width: 40%; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /tests/codeception/_support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | waitForText($text, 10, '#cfiles-crumb'); 37 | } 38 | 39 | public function dontSeeInCrumb($text) 40 | { 41 | $this->dontSee($text, '#cfiles-crumb'); 42 | } 43 | 44 | public function seeInFileList($text) 45 | { 46 | $this->waitForElement('#fileList'); 47 | $this->waitForText($text); 48 | } 49 | 50 | public function dontSeeInFileList($text) 51 | { 52 | $this->dontSee($text, '#fileList'); 53 | } 54 | 55 | public function openFolder($text) 56 | { 57 | $this->click($text, '#fileList'); 58 | $this->seeInCrumb($text); 59 | } 60 | 61 | public function amInRoot() 62 | { 63 | $this->click('.fa-home', '#cfiles-crumb'); 64 | $this->waitForText('Files from the stream', 10, '#fileList'); 65 | } 66 | 67 | public function uploadFile($file = "test.txt") 68 | { 69 | $this->attachFile('#cfilesUploadFiles', $file); 70 | $this->wait(2); 71 | $this->waitForText($file, 10, '#fileList'); 72 | } 73 | 74 | public function rightClickFolder($id) 75 | { 76 | $this->clickWithRightButton('[data-cfiles-item="folder_' . $id . '"]'); 77 | } 78 | 79 | public function rightClickFile($id) 80 | { 81 | $this->clickWithRightButton('[data-cfiles-item="file_' . $id . '"]'); 82 | } 83 | 84 | public function clickFileContext($id, $menuItem) 85 | { 86 | $this->rightClickFile($id); 87 | $this->waitForText($menuItem, 10, '[data-cfiles-item="file_' . $id . '"] .contextMenu'); 88 | $this->click($menuItem, '[data-cfiles-item="file_' . $id . '"] .contextMenu'); 89 | } 90 | 91 | public function clickFolderContext($id, $menuItem) 92 | { 93 | $this->rightClickFolder($id); 94 | $this->waitForText($menuItem, 10, '[data-cfiles-item="folder_' . $id . '"] .contextMenu'); 95 | $this->click($menuItem, '[data-cfiles-item="folder_' . $id . '"] .contextMenu'); 96 | } 97 | 98 | public function importZip($file = "test.zip") 99 | { 100 | $this->attachFile('#cfilesUploadZipFile', $file); 101 | $this->waitForText($file, 10, '#fileList'); 102 | } 103 | 104 | public function enableCfilesOnSpace($guid = 1) 105 | { 106 | $this->enableModule($guid, 'cfiles'); 107 | 108 | $this->amOnSpace($guid); 109 | $this->expectTo('see files in the space nav'); 110 | $this->waitForText('Files', 30, '.layout-nav-container'); 111 | 112 | $this->amOnFilesBrowser(); 113 | } 114 | 115 | public function amOnFilesBrowser() 116 | { 117 | $this->amGoingTo('open files module'); 118 | $this->click('Files', '.layout-nav-container'); 119 | $this->waitForText('Files from the stream', 10, '#fileList'); 120 | } 121 | 122 | /** 123 | * Define custom actions here 124 | */ 125 | public function createFolder($title = 'test', $description = 'test description', $isPublic = false) 126 | { 127 | $this->click('Add directory', '.files-action-menu'); 128 | 129 | $this->waitForText('Create folder', 10, '#globalModal'); 130 | $this->fillField('Folder[title]', $title); 131 | $this->fillField('Folder[description]', $description); 132 | 133 | if ($isPublic) { 134 | $this->jsClick('input#folder-visibility'); 135 | } 136 | 137 | $this->click('Save', '#globalModal'); 138 | $this->waitForText('This folder is empty.', 10, '.folderEmptyMessage'); 139 | } 140 | 141 | public function seeFileSizeOnSpaceStream(BaseFile $file, $guid = 1) 142 | { 143 | $this->amOnSpace($guid); 144 | $this->waitForText($file->file_name); 145 | $this->see('Size: ' . Yii::$app->formatter->asShortSize($file->size, 1)); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /libs/ZIPCreator.php: -------------------------------------------------------------------------------- 1 | cleanup(); 44 | 45 | $this->archive = new ZipArchive(); 46 | $this->tempFile = $this->getTempPath() . DIRECTORY_SEPARATOR . time(); 47 | 48 | $code = $this->archive->open($this->tempFile, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE); 49 | if ($code !== true) { 50 | throw new Exception(Yii::t('CfilesModule.base', 'Opening archive failed with error code %code%.', ['%code%' => $code])); 51 | } 52 | } 53 | 54 | /** 55 | * Closes and writes the ZIP File 56 | */ 57 | public function close() 58 | { 59 | Yii::info('Written ZIP: ' . $this->tempFile . '(Number of files: ' . $this->archive->numFiles . ' - Status: ' . $this->archive->status . ')', 'cfiles'); 60 | $this->archive->close(); 61 | } 62 | 63 | /** 64 | * Returns the temporary generated ZIP File name 65 | * 66 | * @return string the ZIP filename 67 | */ 68 | public function getZipFile() 69 | { 70 | return $this->tempFile; 71 | } 72 | 73 | /** 74 | * Adds file or folder to the ZIP 75 | * 76 | * @param FileSystemItem $item the item 77 | * @param string $path path in the ZIP archive 78 | * @throws Exception 79 | */ 80 | public function add(FileSystemItem $item, $path = '') 81 | { 82 | if ($item instanceof CFile) { 83 | $this->addFile($item, $path); 84 | } elseif ($item instanceof Folder) { 85 | $this->addFolder($item, $path); 86 | } else { 87 | throw new Exception("Invalid file system item given to add to ZIP!"); 88 | } 89 | } 90 | 91 | /** 92 | * Add a file to zip file. 93 | * 94 | * @param File $file the parent folder id which content should be added to the zip file 95 | * @param string $path where we currently are in the zip file 96 | * @param string $fileName alternative fileName otherwise use db filename 97 | */ 98 | public function addFile($file, $path = '', $fileName = null) 99 | { 100 | if ($file instanceof CFile) { 101 | $file = $file->baseFile; 102 | } 103 | 104 | if (!$file || !$file->canView()) { 105 | return; 106 | } 107 | 108 | if ($fileName === null) { 109 | $filePath = $path . DIRECTORY_SEPARATOR . $file->file_name; 110 | } else { 111 | $filePath = $path . DIRECTORY_SEPARATOR . $fileName; 112 | } 113 | 114 | $filePath = $this->fixPath($filePath); 115 | 116 | $realFilePath = $file->store->get(); 117 | if (is_file($realFilePath)) { 118 | $this->archive->addFile($realFilePath, $filePath); 119 | } 120 | } 121 | 122 | /** 123 | * Add files and sub-directories in a folder to zip file. 124 | * 125 | * @param int $folderId the parent folder id which content should be added to the zip file 126 | * @param string $path where we currently are in the zip file 127 | */ 128 | public function addFolder(Folder $folder, $path = '') 129 | { 130 | if (!$folder->content->canView()) { 131 | return; 132 | } 133 | 134 | $path = $this->fixPath($path . DIRECTORY_SEPARATOR . $folder->title); 135 | 136 | $this->archive->addEmptyDir($path); 137 | 138 | $subFiles = CFile::find()->readable()->where(['parent_folder_id' => $folder->id])->all(); 139 | $subFolders = Folder::find()->readable()->where(['parent_folder_id' => $folder->id])->all(); 140 | 141 | foreach ($subFiles as $file) { 142 | $this->addFile($file, $path); 143 | } 144 | 145 | foreach ($subFolders as $folder) { 146 | // checkout subfolders recursively with adapted local path 147 | $this->addFolder($folder, $path); 148 | } 149 | } 150 | } 151 | --------------------------------------------------------------------------------