Browse Source

#3068 Beta version

Alexander 5 years ago
parent
commit
e6f2788ae2

+ 4 - 0
modules/boonex/files/classes/BxFilesConfig.php

@@ -31,6 +31,7 @@ class BxFilesConfig extends BxBaseModTextConfig
             'TABLE_ENTRIES_FULLTEXT' => 'search_fields',
             'TABLE_FILES' => $aModule['db_prefix'] . 'files',
             'TABLE_BOOKMARKS' => $aModule['db_prefix'] . 'bookmarks',
+            'TABLE_FOLDERS' => $aModule['db_prefix'] . 'folders',
 
             // database fields
             'FIELD_ID' => 'id',
@@ -82,6 +83,7 @@ class BxFilesConfig extends BxBaseModTextConfig
             'PARAM_NUM_RSS' => 'bx_files_rss_num',
             'PARAM_SEARCHABLE_FIELDS' => 'bx_files_searchable_fields',
             'PARAM_LINK_TO_PREVIEW' => 'bx_files_show_link_to_preview',
+            'PARAM_MAX_NESTING_LEVEL' => 'bx_files_max_nesting_level',
 
             // objects
             'OBJECT_STORAGE' => 'bx_files_files',
@@ -100,6 +102,8 @@ class BxFilesConfig extends BxBaseModTextConfig
             'OBJECT_CATEGORY' => 'bx_files_cats',
             'OBJECT_PRIVACY_VIEW' => 'bx_files_allow_view_to',
             'OBJECT_PRIVACY_LIST_VIEW' => 'bx_files_allow_view_favorite_list',
+            'OBJECT_FORM_ENTRY_UPLOAD' => 'bx_files_upload',
+            'OBJECT_FORM_ENTRY_DISPLAY_UPLOAD' => 'bx_files_entry_upload',
             'OBJECT_FORM_ENTRY' => 'bx_files',
             'OBJECT_FORM_ENTRY_DISPLAY_VIEW' => 'bx_files_entry_view',
             'OBJECT_FORM_ENTRY_DISPLAY_ADD' => 'bx_files_entry_edit',

+ 113 - 0
modules/boonex/files/classes/BxFilesDb.php

@@ -74,6 +74,119 @@ class BxFilesDb extends BxBaseModFilesDb
             'title' => $sNewTitle,
         ]);
     }
+
+    public function createFolder($iParentFolder, $iAuthor, $iContext, $sTitle) {
+        $CNF = &$this->_oConfig->CNF;
+
+        $this->query("
+            INSERT INTO `" . $CNF['TABLE_ENTRIES'] . "` 
+            (`{$CNF['FIELD_AUTHOR']}`, `{$CNF['FIELD_ADDED']}`, `{$CNF['FIELD_CHANGED']}`, `{$CNF['FIELD_TITLE']}`, `data_processed`, `{$CNF['FIELD_ALLOW_VIEW_TO']}`, `type`, `parent_folder_id`)
+            VALUES (:author, :when, :when, :title, 1, :context, 'folder', :parent_folder_id)", [
+                'author' => $iAuthor,
+                'when' => time(),
+                'title' => $sTitle,
+                'context' => $iContext,
+                'parent_folder_id' => $iParentFolder,
+        ]);
+    }
+
+    public function moveFilesToFolder($aFiles, $iFolder) {
+        $CNF = &$this->_oConfig->CNF;
+        $this->query("UPDATE `" . $CNF['TABLE_ENTRIES'] . "` SET `parent_folder_id` = {$iFolder} WHERE `" . $CNF['FIELD_ID'] . "` IN (".implode(',', $aFiles).")");
+    }
+
+    public function getParentFolderId($iFolder) {
+        $CNF = &$this->_oConfig->CNF;
+        return $this->getOne("SELECT `parent_folder_id` FROM `" . $CNF['TABLE_ENTRIES'] . "` WHERE `" . $CNF['FIELD_ID'] . "` = :folder", [
+            'folder' => $iFolder,
+        ]);
+    }
+
+    public function getFolderFiles($iFolder) {
+        $CNF = &$this->_oConfig->CNF;
+        return $this->getColumn("SELECT `{$CNF['FIELD_ID']}` FROM `" . $CNF['TABLE_ENTRIES'] . "` WHERE `parent_folder_id` = :folder", [
+            'folder' => $iFolder,
+        ]);
+    }
+
+    public function getFolderFilesEx($iFile, $sType = 'folder') {
+        $CNF = &$this->_oConfig->CNF;
+        return $this->getAll("
+
+            SELECT `{$CNF['TABLE_ENTRIES']}`.`{$CNF['FIELD_ID']}`, `{$CNF['TABLE_ENTRIES']}`.`{$CNF['FIELD_AUTHOR']}`, `{$CNF['TABLE_ENTRIES']}`.`{$CNF['FIELD_TITLE']}`, `{$CNF['TABLE_ENTRIES']}`.`type`, `{$CNF['TABLE_FILES']}`.`path`, `{$CNF['TABLE_FILES']}`.`ext`, `{$CNF['TABLE_FILES']}`.`size` 
+            FROM `{$CNF['TABLE_ENTRIES']}` 
+            LEFT JOIN `{$CNF['TABLE_FILES']}` ON `{$CNF['TABLE_ENTRIES']}`.`{$CNF['FIELD_FILE_ID']}` = `{$CNF['TABLE_FILES']}`.`{$CNF['FIELD_ID']}`
+            WHERE `{$CNF['TABLE_ENTRIES']}`.`".($sType == 'folder' ? 'parent_folder_id' : $CNF['FIELD_ID'])."` = :file
+            
+            ", [
+                'file' => $iFile,
+            ]
+        );
+    }
+
+    public function getFolderNestingLevel($iFolder) {
+        $iLevel = 1;
+
+        while($iParentFolder = $this->getParentFolderId($iFolder)) {
+            $iLevel++;
+            $iFolder = $iParentFolder;
+        }
+
+        return $iLevel;
+    }
+
+    public function getFolderPath($iFolder) {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aPath = [];
+
+        $aFolder = $this->getContentInfoById($iFolder);
+        if (!$aFolder) return $aPath;
+
+        do {
+            $aPath[] = [
+                'folder' => $aFolder[$CNF['FIELD_ID']],
+                'name' => $aFolder[$CNF['FIELD_TITLE']],
+            ];
+
+            if (!$aFolder['parent_folder_id']) break;
+            $aFolder = $this->getContentInfoById($aFolder['parent_folder_id']);
+        } while (true);
+
+        return array_reverse($aPath);
+    }
+
+    public function getFoldersInContext($iContext) {
+        $CNF = &$this->_oConfig->CNF;
+        $sIdent = $iContext > 0 ? $CNF['FIELD_AUTHOR'] : $CNF['FIELD_ALLOW_VIEW_TO'];
+        $aFolders = $this->getAll("SELECT `{$CNF['FIELD_ID']}`, `parent_folder_id`, `{$CNF['FIELD_TITLE']}` FROM `" . $CNF['TABLE_ENTRIES'] . "` WHERE `{$sIdent}` = :context AND `type` = 'folder'", [
+            'context' => $iContext,
+        ]);
+
+        if (!$aFolders) return [];
+
+        return [
+            0 => [
+                $CNF['FIELD_ID'] => 0,
+                $CNF['FIELD_TITLE'] => _t('_bx_files_txt_folder_root'),
+                'subfolders' => $this->getSubfoldersOf($aFolders, 0),
+            ]
+        ];
+    }
+
+    public function getSubfoldersOf(&$aFolders, $iParent) {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aResult = [];
+        foreach ($aFolders as $aFolder) {
+            if ($aFolder['parent_folder_id'] == $iParent) {
+                $aFolder['subfolders'] = $this->getSubfoldersOf($aFolders, $aFolder[$CNF['FIELD_ID']]);
+                $aResult[] = $aFolder;
+            }
+        }
+
+        return $aResult;
+    }
 }
 
 /** @} */

+ 284 - 14
modules/boonex/files/classes/BxFilesModule.php

@@ -53,10 +53,16 @@ class BxFilesModule extends BxBaseModTextModule
         return _t('_sys_txt_access_denied');
     }
 
-    public function actionDownload($sToken, $iContentId) 
+    public function actionDownload($sToken, $iContentId, $sBulk = '')
     {
         $CNF = $this->_oConfig->CNF;
-        
+
+        $aFilesToDownload = [$iContentId];
+        if (!empty($sBulk)) {
+            $aFilesToDownload = json_decode($sBulk);
+        }
+        $iContentId = reset($aFilesToDownload);
+
         $aData = $this->_oDb->getContentInfoById((int)$iContentId);
         if (!$aData) {
             $this->_oTemplate->displayPageNotFound();
@@ -68,6 +74,18 @@ class BxFilesModule extends BxBaseModTextModule
             return;
         }
 
+        if ($aData['type'] == 'folder' || $aFilesToDownload) {
+            $sZipFilePath = $this->prepareFolderForDownloading($aFilesToDownload);
+            if (!$sZipFilePath) {
+                $this->_oTemplate->displayPageNotFound();
+                return;
+            }
+
+            $sFolderName = $this->sanitizeFileName($aData[$CNF['FIELD_TITLE']]).'.zip';
+            bx_smart_readfile($sZipFilePath, $sFolderName, 'application/zip');
+            exit;
+        }
+
         $aFile = $this->getContentFile($aData);
         if (!$aFile) {
             $this->_oTemplate->displayPageNotFound();
@@ -202,10 +220,19 @@ class BxFilesModule extends BxBaseModTextModule
     public function actionBookmark($iContentId) {
         if (!$this->isLogged()) return;
 
-        $aData = $this->_oDb->getContentInfoById((int)$iContentId);
-        if (!$aData) return;
+        $aFilesToBookmark = [];
+        if ($iContentId) $aFilesToBookmark = [$iContentId];
+        elseif (isset($_POST['bulk'])) $aFilesToBookmark = $_POST['bulk'];
+
+        if ($aFilesToBookmark)
+            foreach ($aFilesToBookmark as $iContentId) {
+                if ($iContentId <= 0) continue;
 
-        $this->_oDb->bookmarkFile((int)$iContentId, bx_get_logged_profile_id());
+                $aData = $this->_oDb->getContentInfoById((int)$iContentId);
+                if (!$aData) continue;
+
+                $this->_oDb->bookmarkFile((int)$iContentId, bx_get_logged_profile_id());
+            }
     }
 
     public function actionEntryPreview($iContentId) {
@@ -294,27 +321,184 @@ class BxFilesModule extends BxBaseModTextModule
     }
 
     public function actionEntryDelete($iContentId) {
-        $iContentId = intval($iContentId);
-        $aData = $this->_oDb->getContentInfoById($iContentId);
+        $aFilesToDelete = [];
+        if ($iContentId) $aFilesToDelete = [$iContentId];
+        elseif (isset($_POST['bulk'])) $aFilesToDelete = $_POST['bulk'];
 
-        $CNF = &$this->_oConfig->CNF;
+        if ($aFilesToDelete)
+            foreach ($aFilesToDelete as $iContentId) {
+                $iContentId = intval($iContentId);
+                if ($iContentId <= 0) continue;
+
+                $aData = $this->_oDb->getContentInfoById($iContentId);
+
+                $CNF = &$this->_oConfig->CNF;
+
+                if (!$aData) {
+                    echoJson(['message' => _t('_bx_files_txt_error')]);
+                    exit;
+                }
+
+                if ($aData[$CNF['FIELD_AUTHOR']] != bx_get_logged_profile_id() && !($aData[$CNF['FIELD_ALLOW_VIEW_TO']] < 0 && $this->serviceIsAllowedAddContentToProfile(-$aData[$CNF['FIELD_ALLOW_VIEW_TO']]))) {
+                    echoJson(['message' => _t('_bx_files_txt_permission_denied')]);
+                    exit;
+                }
+
+                $this->serviceDeleteEntity($iContentId);
+            }
+
+        if (!isset($_POST['bulk'])) {
+            echoJson([
+                'eval' => "
+                    $('.bx-file-entry[file_id={$iContentId}]').fadeOut();
+                    $(window).trigger('files_browser.update');
+                ",
+            ]);
+        } else {
+            echoJson([]);
+        }
+    }
+
+    public function actionClearGhosts() {
+        if (!isLogged()) die;
+
+        $oStorage = BxDolStorage::getObjectInstance($this->_oConfig->CNF['OBJECT_STORAGE']);
+        if ($oStorage) {
+            $aFiles = $oStorage->getGhosts(bx_get_logged_profile_id(), 0);
+
+            if ($aFiles) foreach ($aFiles as $aFile) {
+                $oStorage->deleteFile($aFile['id']);
+            }
+        }
+    }
+
+    public function actionUploadCompleted() {
+        $iContext = intval(bx_get('context'));
+        $iFolder = intval(bx_get('folder'));
+
+        if (!isLogged()) {
+            echoJson(['message' => _t('_Access denied')]);
+            exit;
+        }
+
+        if ($this->checkAllowedAdd() != CHECK_ACTION_RESULT_ALLOWED || !$this->serviceIsAllowedAddContentToProfile($iContext)) {
+            echoJson(['message' => _t('_Access denied')]);
+            exit;
+        }
+
+        $oStorage = BxDolStorage::getObjectInstance($this->_oConfig->CNF['OBJECT_STORAGE']);
+        if ($oStorage) {
+            $iAuthorProfile = bx_get_logged_profile_id();
+            $aFiles = $oStorage->getGhosts($iAuthorProfile, 0);
+
+            if ($aFiles) {
+                $aFilesIDs = [];
+                foreach($aFiles as $aFile) {
+                    $aFilesIDs[] = $aFile['id'];
+                    bx_set('title-'.$aFile['id'], $aFile['file_name'], 'post');
+                }
+                bx_set('attachments', $aFilesIDs, 'post');
+
+                $oForm = BxDolForm::getObjectInstance($this->_oConfig->CNF['OBJECT_FORM_ENTRY_UPLOAD'], $this->_oConfig->CNF['OBJECT_FORM_ENTRY_DISPLAY_UPLOAD']);
+                $oForm->aInputs['profile_id']['value'] = $iAuthorProfile;
+                $oForm->aInputs['allow_view_to']['value'] = $iContext == $iAuthorProfile ? 3 : -$iContext;
+
+                $aFiles = $oForm->insert();
+
+                if ($iFolder != 0 && !empty($aFiles))
+                    $this->_oDb->moveFilesToFolder($aFiles, $iFolder);
+            }
+        }
+    }
+
+    public function actionCreateFolder() {
+        $iContext = intval(bx_get('context'));
+        $iFolder = intval(bx_get('current_folder'));
+        $sName = trim(bx_get('name'));
+
+        if (!isLogged()) {
+            echoJson(['message' => _t('_Access denied')]);
+            exit;
+        }
+
+        if ($this->checkAllowedAdd() != CHECK_ACTION_RESULT_ALLOWED || !$this->serviceIsAllowedAddContentToProfile($iContext)) {
+            echoJson(['message' => _t('_Access denied')]);
+            exit;
+        }
+
+        if (!empty($sName)) {
+            $iAuthor = bx_get_logged_profile_id();
+            //in case it is posted to context make the context profile as an author of an entry
+            $this->_oDb->createFolder($iFolder, $iAuthor == $iContext ? $iAuthor : $iContext, $iAuthor == $iContext ? BX_DOL_PG_ALL : -$iContext, $sName);
+        }
+
+        echoJson([]);
+        exit;
+    }
+
+    public function actionMoveFiles() {
+        $iContentId = intval(bx_get('file'));
+
+        $aFilesToMove = [];
+        if ($iContentId) $aFilesToMove = [$iContentId];
+        elseif (isset($_POST['bulk'])) $aFilesToMove = $_POST['bulk'];
+
+        $iContentId = reset($aFilesToMove);
+
+        $aData = $this->_oDb->getContentInfoById($iContentId);
 
         if (!$aData) {
             echoJson(['message' => _t('_bx_files_txt_error')]);
             exit;
         }
 
-        if ($aData[$CNF['FIELD_AUTHOR']] != bx_get_logged_profile_id()) {
-            echoJson(['message' => _t('_bx_files_txt_permission_denied')]);
+        $CNF = &$this->_oConfig->CNF;
+
+        $iContext = $aData[$CNF['FIELD_ALLOW_VIEW_TO']] < 0 ? -$aData[$CNF['FIELD_ALLOW_VIEW_TO']] : $aData[$CNF['FIELD_AUTHOR']];
+
+        if ($this->checkAllowedAdd() != CHECK_ACTION_RESULT_ALLOWED || !$this->serviceIsAllowedAddContentToProfile($iContext)) {
+            echoJson(['message' => _t('_Access denied')]);
             exit;
         }
 
-        $this->serviceDeleteEntity($iContentId);
+        if (isset($_POST['move_to'])) {
+            $iMoveTo = intval(bx_get('move_to'));
+            if ($iMoveTo) {
+                $aDataMoveTo = $this->_oDb->getContentInfoById($iMoveTo);
+                if (!$aDataMoveTo) {
+                    echoJson(['message' => _t('_bx_files_txt_error')]);
+                    exit;
+                }
+
+                $iContextMoveTo = $aDataMoveTo[$CNF['FIELD_ALLOW_VIEW_TO']] < 0 ? -$aDataMoveTo[$CNF['FIELD_ALLOW_VIEW_TO']] : $aDataMoveTo[$CNF['FIELD_AUTHOR']];
+
+                if ($this->checkAllowedAdd() != CHECK_ACTION_RESULT_ALLOWED || !$this->serviceIsAllowedAddContentToProfile($iContextMoveTo)) {
+                    echoJson(['message' => _t('_Access denied')]);
+                    exit;
+                }
+            }
+
+            $this->_oDb->moveFilesToFolder($aFilesToMove, $iMoveTo);
+
+            echoJson([
+                'eval' => "                        
+                    $(window).trigger('files_browser.update');
+                ",
+            ]);
+            exit;
+        }
+
+        $aFolders = $this->_oDb->getFoldersInContext($iContext);
+        if (!$aFolders) {
+            echoJson(['message' => _t('_bx_files_txt_move_to_nowhere')]);
+            exit;
+        }
 
         echoJson([
-            'eval' => "
-                $('.bx-file-entry[file_id={$iContentId}]').fadeOut();
-            ",
+            'popup' => [
+                'html' => PopupBox('bx-files-popup', _t('_bx_files_txt_move_to'), $this->_oTemplate->getJsTree($aFilesToMove, $aFolders)),
+                'options' => [],
+            ],
         ]);
     }
 
@@ -322,6 +506,92 @@ class BxFilesModule extends BxBaseModTextModule
         $this->_oDb->deleteProfileBookmarks($iProfileId);
         return parent::serviceDeleteEntitiesByAuthor ($iProfileId);
     }
+
+    public function serviceDeleteEntity ($iContentId, $sFuncDelete = 'deleteData') {
+        $aData = $this->_oDb->getContentInfoById($iContentId);
+        if ($aData['type'] == 'folder') {
+            $aNestedFiles = $this->_oDb->getFolderFiles($aData[$this->_oConfig->CNF['FIELD_ID']]);
+            if ($aNestedFiles)
+                foreach ($aNestedFiles as $iFile) {
+                    $this->serviceDeleteEntity($iFile);
+                }
+        }
+        parent::serviceDeleteEntity($iContentId, $sFuncDelete);
+    }
+
+    private function prepareFolderForDownloading($aFiles) {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aFilesList = [];
+        foreach ($aFiles as $iFile) {
+            $aData = $this->_oDb->getContentInfoById(intval($iFile));
+            $aFilesList = array_merge($aFilesList, $this->getFolderFiles(intval($iFile), $aData['type'] == 'folder' ? $this->sanitizeFileName($aData['title']).'/' : '/', $aData['type']));
+        }
+        if(!$aFilesList) return false;
+
+        $sZipFile = BX_DIRECTORY_PATH_TMP.'files-folder-'.mt_rand().'.zip';
+        if (file_exists($sZipFile)) @unlink($sZipFile);
+
+        $oZipFile = new ZipArchive();
+        if ($oZipFile->open($sZipFile, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE)) {
+            $oZipFile->setCompressionIndex(0, ZipArchive::CM_STORE);
+
+            foreach ($aFilesList as $aFile) {
+                $sFilePath = BX_DIRECTORY_STORAGE . $CNF['OBJECT_STORAGE'].'/'.$aFile['path'];
+                if (file_exists($sFilePath) && is_readable($sFilePath))
+                    $oZipFile->addFile($sFilePath, $aFile['filename']);
+            }
+
+            $oZipFile->close();
+            return $sZipFile;
+        } else {
+            return false;
+        }
+    }
+
+    private function getFolderFiles($iFile, $sRelPath, $sType) {
+        $aFiles = $this->_oDb->getFolderFilesEx($iFile, $sType);
+        if (!$aFiles) return [];
+
+        $aResult = [];
+        $aUniqueNames = [];
+        foreach ($aFiles as $aFile) {
+            if (CHECK_ACTION_RESULT_ALLOWED !== $this->checkAllowedView($aFile)) continue;
+
+            //in case title has been changed we might have to add extensions manually
+            $aFileInfo = pathinfo($aFile['title']);
+            $sFileBaseName = isset($aFileInfo['filename']) ? $aFileInfo['filename'] : '_';
+            $sFileExt = isset($aFileInfo['extension']) ? $aFileInfo['extension'] : '';
+
+            if (!$sFileExt || $sFileExt != $aFile['ext']) $sFileExt = $aFile['ext'];
+
+            //handle multiple files with the same name
+            $sFilename = $this->sanitizeFileName($sFileBaseName);
+            $iIndex = 1;
+            while (isset($aUniqueNames[$sFilename])) {
+                $sFilename = $this->sanitizeFileName($sFileBaseName.'_'.$iIndex++);
+            }
+            $aUniqueNames[$sFilename] = 1;
+
+            if ($aFile['type'] == 'folder') {
+                $aResult = array_merge($aResult, $this->getFolderFiles($aFile['id'], $sRelPath . $sFilename . '/', 'folder'));
+            } else {
+                $aResult[] = [
+                    'filename' => trim($sRelPath.$sFilename.'.'.$sFileExt, '/'),
+                    'size' => $aFile['size'],
+                    'path' => $aFile['path'],
+                ];
+            }
+        }
+
+        return $aResult;
+    }
+
+    private function sanitizeFileName($sDesiredFilename) {
+        $sDesiredFilename = mb_ereg_replace("([^\w\s\d\-_~,;\[\]\(\).])", '_', $sDesiredFilename);
+        $sDesiredFilename = mb_ereg_replace("([\.]{2,})", '_', $sDesiredFilename);
+        return $sDesiredFilename;
+    }
 }
 
 /** @} */

+ 133 - 10
modules/boonex/files/classes/BxFilesSearchResult.php

@@ -14,6 +14,7 @@ class BxFilesSearchResult extends BxBaseModTextSearchResult
     protected $sOrderParamName = 'order';
     protected $sBookmarksParamName = 'bookmarks';
     protected $sSortingParamName = 'sorting';
+    protected $sCurrentFolderParamName = 'folder';
     protected $sCurrentView;
     protected $bFileManagerMode = false;
     protected $iFileManagerUploadTo = 0;
@@ -29,7 +30,7 @@ class BxFilesSearchResult extends BxBaseModTextSearchResult
         ];
 
         //Enble filemanager (toolbar, table files layout, dynamic menus, folders support) only for author/context blocks.
-        if (($sMode == 'author' || $sMode == 'context')) {
+        if (($sMode == 'author' || $sMode == 'context') && !isset($aParams['no_toolbar'])) {
             $this->bFileManagerMode = true;
             $this->setUnitParams(['show_inline_menu' => true]);
         }
@@ -43,6 +44,7 @@ class BxFilesSearchResult extends BxBaseModTextSearchResult
             $this->sBookmarksParamName,
             $this->sSortingParamName,
             $this->sUnitViewParamName,
+            $this->sCurrentFolderParamName,
         ]);
 
 
@@ -60,13 +62,16 @@ class BxFilesSearchResult extends BxBaseModTextSearchResult
             'object_metatags' => 'bx_files',
             'title' => _t('_bx_files_page_title_browse'),
             'table' => 'bx_files_main',
-            'ownFields' => array('id', 'file_id', 'title', 'desc', 'author', 'added'),
+            'ownFields' => array('id', 'file_id', 'title', 'desc', 'author', 'added', 'type', 'parent_folder_id'),
             'searchFields' => array(),
             'restriction' => array(
                 'author' => array('value' => '', 'field' => 'author', 'operator' => '='),
                 'featured' => array('value' => '', 'field' => 'featured', 'operator' => '<>'),
                 'status' => array('value' => 'active', 'field' => 'status', 'operator' => '='),
                 'statusAdmin' => array('value' => 'active', 'field' => 'status_admin', 'operator' => '='),
+                'files_only' => array('value' => 'file', 'field' => 'type', 'operator' => '='),
+                'context_filter' => array('value' => '', 'field' => 'allow_view_to', 'operator' => '>'),
+                'folder' => array('value' => '', 'field' => 'parent_folder_id', 'operator' => '='),
             ),
             'paginate' => array('perPage' => getParam('bx_files_per_page_browse'), 'start' => 0),
             'sorting' => 'last',
@@ -101,6 +106,10 @@ class BxFilesSearchResult extends BxBaseModTextSearchResult
             case 'author':
                 if(!$this->_updateCurrentForAuthor($sMode, $aParams, $oProfileAuthor))
                     $this->isError = true;
+                else {
+                    //show only those files/folders which are not posted to some context
+                    $this->aCurrent['restriction']['context_filter']['value'] = 0;
+                }
                 break;
 
             case 'context':
@@ -163,6 +172,9 @@ class BxFilesSearchResult extends BxBaseModTextSearchResult
         $this->addConditionsForPrivateContent($CNF, $oProfileAuthor);
 
         if ($this->bFileManagerMode) {
+            $this->addContainerClass('bx-files-with-toolbar');
+
+            unset($this->aCurrent['restriction']['files_only']);
             if (bx_get($this->sBookmarksParamName))
                 $this->addConditionsForBookmarks($CNF);
 
@@ -174,7 +186,26 @@ class BxFilesSearchResult extends BxBaseModTextSearchResult
 
             $this->removeContainerClass('bx-def-margin-sec-lefttopright-neg');
 
-            $this->iFileManagerUploadTo = $oProfileAuthor->id();
+            if (!$this->isError) {
+                if ($sMode == 'author' || $oProfileAuthor->id() != bx_get_logged_profile_id())
+                    $this->iFileManagerUploadTo = $oProfileAuthor->id();
+            }
+
+            if ($this->sCurrentView == 'table')
+                $this->removeContainerClass('bx-def-margin-bottom-neg');
+
+
+            $this->aCurrent['restriction']['folder']['value'] = intval(bx_get($this->sCurrentFolderParamName));
+            if ($this->aCurrent['restriction']['folder']['value'] < 0) {
+                //navigate folder top
+                bx_set($this->sCurrentFolderParamName, $this->oModule->_oDb->getParentFolderId(-$this->aCurrent['restriction']['folder']['value']));
+                $this->aCurrent['restriction']['folder']['value'] = bx_get($this->sCurrentFolderParamName);
+            }
+
+            //free up one place for folder up item
+            if ($this->aCurrent['restriction']['folder']['value']) {
+                $this->aCurrent['paginate']['perPage']--;
+            }
         }
     }
 
@@ -220,12 +251,15 @@ class BxFilesSearchResult extends BxBaseModTextSearchResult
 
         if (!$this->bFileManagerMode) return;
 
+        $this->oModule->_oTemplate->addCss('main.css');
+
         return $this->showBrowseToolbar([
             'aRequestParams' => [
                 'unit_view_param' => $this->sUnitViewParamName,
                 'bookmarks_param' => $this->sBookmarksParamName,
                 'sorting_param' => $this->sSortingParamName,
                 'keyword_param' => 'keyword',
+                'current_folder' => $this->sCurrentFolderParamName,
             ],
 
             'layout' => $this->sCurrentView,
@@ -233,13 +267,37 @@ class BxFilesSearchResult extends BxBaseModTextSearchResult
             'sorting' => $this->aCurrent['sorting'],
             'sorting_options' => $this->aSortingOptions,
             'keyword' => bx_get('keyword') ? bx_get('keyword') : '',
+            'current_folder' => $this->aCurrent['restriction']['folder']['value'],
+            'current_page' => $this->aCurrent['paginate']['start'],
         ]);
     }
 
+    function getSearchData ()
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+
+        $aData = parent::getSearchData();
+
+        if ($this->bFileManagerMode && $this->aCurrent['restriction']['folder']['value']) {
+            $aData = array_merge([0 => [
+                $CNF['FIELD_ID'] => '-1',
+                $CNF['FIELD_FILE_ID'] => '0',
+                $CNF['FIELD_TITLE'] => '..',
+                $CNF['FIELD_AUTHOR'] => 0,
+                $CNF['FIELD_ADDED'] => 0,
+                $CNF['FIELD_ALLOW_VIEW_TO'] => BX_DOL_PG_ALL,
+                'type' => 'folder',
+            ]], $aData);
+            $this->aCurrent['paginate']['num']++;
+        }
+
+        return $aData;
+    }
+
     function processing ()
     {
         $sCode = parent::processing();
-        if (!$this->aCurrent['paginate']['num'] && bx_is_dynamic_request() && $this->bFileManagerMode) {
+        if (!$this->aCurrent['paginate']['num']  && $this->bFileManagerMode) {
             //to show toolbar in case of empty results
             $sCode = $this->displaySearchBox($this->addCustomParts().MsgBox(_t('_Empty')));
         }
@@ -252,6 +310,7 @@ class BxFilesSearchResult extends BxBaseModTextSearchResult
             $this->sUnitViewParamName => $this->sCurrentView,
             $this->sBookmarksParamName => intval(bx_get($this->sBookmarksParamName)),
             $this->sSortingParamName => $this->aCurrent['sorting'],
+            $this->sCurrentFolderParamName => $this->aCurrent['restriction']['folder']['value'],
         ];
         $sPageUrl = $this->getCurrentUrl($aAdditionalParams, false);
         $sOnClick = $this->getCurrentOnclick($aAdditionalParams, false);
@@ -274,16 +333,16 @@ class BxFilesSearchResult extends BxBaseModTextSearchResult
             $aSql = array();
             switch ($this->aCurrent['sorting']) {
                 case 'name':
-                    $aSql['order'] = ' ORDER BY `'.$CNF['TABLE_ENTRIES'].'`.`'.$CNF['FIELD_TITLE'].'` ASC';
+                    $aSql['order'] = ' ORDER BY `'.$CNF['TABLE_ENTRIES'].'`.`type` DESC, `'.$CNF['TABLE_ENTRIES'].'`.`'.$CNF['FIELD_TITLE'].'` ASC';
                     break;
                 case 'date':
-                    $aSql['order'] = ' ORDER BY `'.$CNF['TABLE_ENTRIES'].'`.`'.$CNF['FIELD_ID'].'` DESC';
+                    $aSql['order'] = ' ORDER BY `'.$CNF['TABLE_ENTRIES'].'`.`type` DESC, `'.$CNF['TABLE_ENTRIES'].'`.`'.$CNF['FIELD_ID'].'` DESC';
                     break;
                 case 'author':
-                    $aSql['order'] = ' ORDER BY `'.$CNF['TABLE_ENTRIES'].'`.`'.$CNF['FIELD_AUTHOR'].'` ASC, `'.$CNF['TABLE_ENTRIES'].'`.`'.$CNF['FIELD_TITLE'].'` ASC';
+                    $aSql['order'] = ' ORDER BY `'.$CNF['TABLE_ENTRIES'].'`.`type` DESC, `'.$CNF['TABLE_ENTRIES'].'`.`'.$CNF['FIELD_AUTHOR'].'` ASC, `'.$CNF['TABLE_ENTRIES'].'`.`'.$CNF['FIELD_TITLE'].'` ASC';
                     break;
                 case 'type':
-                    $aSql['order'] = ' ORDER BY `'.$CNF['TABLE_FILES'].'`.`'.$CNF['FIELD_MIME_TYPE'].'` ASC, `'.$CNF['TABLE_ENTRIES'].'`.`'.$CNF['FIELD_TITLE'].'` ASC';
+                    $aSql['order'] = ' ORDER BY `'.$CNF['TABLE_ENTRIES'].'`.`type` DESC, `'.$CNF['TABLE_FILES'].'`.`'.$CNF['FIELD_MIME_TYPE'].'` ASC, `'.$CNF['TABLE_ENTRIES'].'`.`'.$CNF['FIELD_TITLE'].'` ASC';
                     break;
             }
             return $aSql;
@@ -303,12 +362,15 @@ class BxFilesSearchResult extends BxBaseModTextSearchResult
 
         $sUniqueIdent = mt_rand();
         $aParams['unique_ident'] = $sUniqueIdent;
+        $aParams['context'] = $this->iFileManagerUploadTo;
 
         $this->oModule->_oTemplate->addJs('toolbar_tools.js');
 
         $sJsCode = $this->oModule->_oTemplate->getJsCode(['type' => 'toolbar_tools', 'uniq' => $sUniqueIdent], $aParams);
         $sJsObject = $this->oModule->_oConfig->getJsObject(['type' => 'toolbar_tools', 'uniq' => $sUniqueIdent]);
 
+        $this->setUnitParams(['toolbar_js_object' => $sJsObject]);
+
         $oForm = new BxTemplFormView([], $this->oModule->_oTemplate);
         $aInputSorting = [
             'type' => 'select',
@@ -331,15 +393,75 @@ class BxFilesSearchResult extends BxBaseModTextSearchResult
             ],
         ];
 
+        $aUploadButtonParams = [];
+        $bUploadAllowed = $this->oModule->checkAllowedAdd() == CHECK_ACTION_RESULT_ALLOWED && $this->oModule->serviceIsAllowedAddContentToProfile($this->iFileManagerUploadTo);
+        if ($bUploadAllowed) {
+            $sUniqId = genRndPwd (8, false);
+            $oUploader = BxDolUploader::getObjectInstance('bx_files_html5', $this->oModule->_oConfig->CNF['OBJECT_STORAGE'], $sUniqId, $this->oModule->_oTemplate);
+
+            $aUploaderParams = array(
+                'button_title' => '',
+                'storage_private' => '0',
+                'images_transcoder' => 'bx_files_preview',
+                'attrs' => bx_convert_array2attrs(['style' => 'display:none;']),
+                'btn_class' => '',
+            );
+
+            $iMaxNestingLevel = intval(getParam($this->oModule->_oConfig->CNF['PARAM_MAX_NESTING_LEVEL']));
+            $aUploadButtonParams = [
+                'js_object' => $sJsObject,
+                'uploader_code' => $oUploader->getUploaderButton('', true, $aUploaderParams, true),
+                'uploader_js_object' => $oUploader->getNameJsInstanceUploader(),
+                'uploader_click_handler' => $oUploader->getNameJsInstanceUploader() . '.showUploaderForm();',
+                'bx_if:create_folder_allowed' => [
+                    'condition' => $iMaxNestingLevel == 0 || $this->oModule->_oDb->getFolderNestingLevel($this->aCurrent['restriction']['folder']['value']) < $iMaxNestingLevel,
+                    'content' => [
+                        'js_object' => $sJsObject,
+                        'folder_name_message' => _t('_bx_files_txt_folder_name'),
+                    ]
+                ],
+            ];
+        }
+
+        $aFolderPathTmpl = [];
+        $aFolderPath = $this->oModule->_oDb->getFolderPath($this->aCurrent['restriction']['folder']['value']);
+        if ($aFolderPath) {
+            $aFolderPathTmpl[] = [
+                'name' => _t('_bx_files_txt_folder_root'),
+                'folder' => '0',
+                'js_object' => $sJsObject,
+            ];
+
+            foreach ($aFolderPath as $aEntry) {
+                $aFolderPathTmpl[] = [
+                    'name' => strmaxtextlen($aEntry['name'], 20),
+                    'folder' => $aEntry['folder'],
+                    'js_object' => $sJsObject,
+                ];
+            }
+        }
+
+        $aBulkActions = [];
+        $aBulkActions[] = ['js_object' => $sJsObject, 'handler' => 'downloadFiles', 'title' => _t('_bx_files_bulk_action_title_download'), 'icon' => 'download'];
+        if (isLogged()) {
+            $aBulkActions[] = ['js_object' => $sJsObject, 'handler' => 'bookmarkFiles', 'title' => _t('_bx_files_bulk_action_title_bookmark'), 'icon' => 'star'];
+            if ($bUploadAllowed) {
+                $aBulkActions[] = ['js_object' => $sJsObject, 'handler' => 'deleteFiles', 'title' => _t('_bx_files_bulk_action_title_delete'), 'icon' => 'remove'];
+                $aBulkActions[] = ['js_object' => $sJsObject, 'handler' => 'moveFiles', 'title' => _t('_bx_files_bulk_action_title_move_to'), 'icon' => 'file-export'];
+            }
+        }
+
+
         return $this->oModule->_oTemplate->parseHtmlByName('files_browser_toolbar.html', [
             'js_object' => $sJsObject,
             'js_code' => $sJsCode,
             'unique_ident' => $sUniqueIdent,
             'sorting_dropdown' => $oForm->genRowStandard($aInputSorting),
             'filter_box' => $oForm->genRowStandard($aInputFilter),
+            'bx_repeat:folder_path' => $aFolderPathTmpl,
             'bx_if:upload_visible' => [
-                'condition' => $this->oModule->serviceIsAllowedAddContentToProfile($this->iFileManagerUploadTo),
-                'content' => [],
+                'condition' => $bUploadAllowed,
+                'content' => $aUploadButtonParams,
             ],
             'bx_if:bookmarks_visible' => [
                 'condition' => isLogged(),
@@ -370,6 +492,7 @@ class BxFilesSearchResult extends BxBaseModTextSearchResult
                 ],
             ],
             'bx_repeat:sorting_options' => $aSortingOptions,
+            'bx_repeat:bulk_actions' => $aBulkActions,
         ]);
     }
 }

+ 47 - 5
modules/boonex/files/classes/BxFilesTemplate.php

@@ -38,9 +38,11 @@ class BxFilesTemplate extends BxBaseModTextTemplate
     {
         $oModule = $this->getModule();
 
-    	$sResult = $this->checkPrivacy ($aData, $isCheckPrivateContent, $oModule, $sTemplateName);
-    	if($sResult)
-            return $sResult;
+        if ($aData['type'] != 'folder') {
+            $sResult = $this->checkPrivacy($aData, $isCheckPrivateContent, $oModule, $sTemplateName);
+            if ($sResult)
+                return $sResult;
+        }
 
         $CNF = &$oModule->_oConfig->CNF;
 
@@ -57,7 +59,7 @@ class BxFilesTemplate extends BxBaseModTextTemplate
         $aVars['bx_if:no_thumb']['content']['icon'] = $aVars['icon'];
 
 
-        $aVars['bx_if:inline_menu']['condition'] = isset($aParams['show_inline_menu']) && $aParams['show_inline_menu'];
+        $aVars['bx_if:inline_menu']['condition'] = $aData[$CNF['FIELD_ID']] > 0 && isset($aParams['show_inline_menu']) && $aParams['show_inline_menu'];
         $aVars['bx_if:inline_menu']['content'] = [];
         if ($aVars['bx_if:inline_menu']['condition']) {
             bx_import('BxTemplMenu');
@@ -74,11 +76,17 @@ class BxFilesTemplate extends BxBaseModTextTemplate
         if ($oModule->_oDb->getParam($CNF['PARAM_LINK_TO_PREVIEW'])) {
             $sAltContentUrlHandler = '$.get(\'' . BX_DOL_URL_ROOT . $oModule->_oConfig->getBaseUri() . 'entry_preview/' . $aData[$CNF['FIELD_ID']] . '\', processJsonData, \'json\'); return false;';
         }
+
+        if ($aData['type'] == 'folder') {
+            $aVars['bx_if:meta']['condition'] = false;
+            $aVars['bx_if:no_thumb']['content']['icon'] = $aVars['icon'] = 'folder';
+            $sAltContentUrlHandler = "{$aParams['toolbar_js_object']}.folderNavigate({$aData[$CNF['FIELD_ID']]}); return false;";
+        }
+
         $aVars['alt_content_url_handler'] = $sAltContentUrlHandler;
         $aVars['bx_if:no_thumb']['content']['alt_content_url_handler'] = $sAltContentUrlHandler;
         $aVars['bx_if:thumb']['content']['alt_content_url_handler'] = $sAltContentUrlHandler;
 
-
         return $this->parseHtmlByName($sTemplateName, $aVars);
     }
 
@@ -137,6 +145,11 @@ class BxFilesTemplate extends BxBaseModTextTemplate
             ],
         ];
 
+        if ($aData['type'] == 'folder') {
+            unset($aForm['inputs']['size']);
+            unset($aForm['inputs']['author']);
+        }
+
         if ($aData[$CNF['FIELD_ALLOW_VIEW_TO']] < 0) {
             $oProfile = BxDolProfile::getInstance(-$aData[$CNF['FIELD_ALLOW_VIEW_TO']]);
             if ($oProfile) {
@@ -172,6 +185,35 @@ class BxFilesTemplate extends BxBaseModTextTemplate
 
         return $aVars;
     }
+
+    public function getJsTree($aFilesToMove, $aFolders) {
+        $sJsCss = '';
+        $sJsCss .= $this->addCss(BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'jstree/themes/default/|style.min.css', true);
+        $sJsCss .= $this->addJs(BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'jstree/|jstree.min.js', true);
+
+        return $this->parseHtmlByName('folders_tree.html', [
+            'js_css' => $sJsCss,
+            'list' => $this->foldersToList($aFolders, 0),
+            'files' => json_encode($aFilesToMove),
+            'actions_url' => BX_DOL_URL_ROOT.$this->getModule()->_oConfig->getBaseUri(),
+        ]);
+    }
+
+    private function foldersToList($aFolders, $iLevel) {
+        if (!$aFolders) return;
+
+        $sTreeList = '<ul>';
+        foreach ($aFolders as $aFolder) {
+            $sTreeList .=
+                '<li id="bx-files-folder-'.$aFolder['id'].'" '.($iLevel == 0 ? 'class="jstree-open"' : '').' data-jstree=\'{"icon":"sys-icon folder"}\'>'.
+                    bx_process_output($aFolder['title']).
+                    $this->foldersToList($aFolder['subfolders'], $iLevel+1).
+                '</li>';
+        }
+        $sTreeList .= '</ul>';
+
+        return $sTreeList;
+    }
 }
 
 /** @} */

+ 1 - 1
modules/boonex/files/install/config.php

@@ -18,7 +18,7 @@ $aConfig = array(
     'name' => 'bx_files',
     'title' => 'Files',
     'note' => 'Basic blogging module.',
-    'version' => '11.0.1.DEV',
+    'version' => '11.0.2.DEV',
     'vendor' => 'BoonEx',
     'help_url' => 'http://feed.una.io/?section={module_name}',
 

+ 14 - 0
modules/boonex/files/install/langs/en.xml

@@ -257,6 +257,7 @@
     <string name="_bx_files_menu_item_title_sm_size"><![CDATA[Size]]></string>
 
     <string name="_bx_files_option_link_to_preview"><![CDATA[Link file title to a preview popup instead of a file view page in context blocks]]></string>
+    <string name="_bx_files_option_max_nesting_level"><![CDATA[Folders max nesting level]]></string>
 
     <string name="_bx_files_entry_info_size"><![CDATA[Size]]></string>
     <string name="_bx_files_entry_info_date"><![CDATA[Date]]></string>
@@ -273,4 +274,17 @@
 
     <string name="_bx_files_txt_error"><![CDATA[File not found]]></string>
     <string name="_bx_files_txt_permission_denied"><![CDATA[Permission denied]]></string>
+    <string name="_bx_files_txt_upload"><![CDATA[Upload]]></string>
+    <string name="_bx_files_txt_add_folder"><![CDATA[Add Folder]]></string>
+    <string name="_bx_files_txt_folder_name"><![CDATA[Folder name]]></string>
+    <string name="_bx_files_txt_folder_root"><![CDATA[Root]]></string>
+    <string name="_bx_files_txt_move_to"><![CDATA[Move To]]></string>
+    <string name="_bx_files_txt_move_to_nowhere"><![CDATA[There are no folders created yet]]></string>
+    <string name="_bx_files_txt_move_here"><![CDATA[Move here?]]></string>
+
+    <string name="_bx_files_bulk_action_title_download"><![CDATA[Download]]></string>
+    <string name="_bx_files_bulk_action_title_bookmark"><![CDATA[Bookmark]]></string>
+    <string name="_bx_files_bulk_action_title_delete"><![CDATA[Delete]]></string>
+    <string name="_bx_files_bulk_action_title_move_to"><![CDATA[Move To...]]></string>
+
 </resources>

+ 10 - 8
modules/boonex/files/install/sql/enable.sql

@@ -13,6 +13,7 @@ SET @iCategId = LAST_INSERT_ID();
 INSERT INTO `sys_options` (`name`, `value`, `category_id`, `caption`, `type`, `check`, `check_error`, `extra`, `order`) VALUES
 ('bx_files_summary_chars', '700', @iCategId, '_bx_files_option_summary_chars', 'digit', '', '', '', 1),
 ('bx_files_show_link_to_preview', '', @iCategId, '_bx_files_option_link_to_preview', 'checkbox', '', '', '', 5),
+('bx_files_max_nesting_level', '3', @iCategId, '_bx_files_option_max_nesting_level', 'digit', '', '', '', 6),
 ('bx_files_per_page_browse', '12', @iCategId, '_bx_files_option_per_page_browse', 'digit', '', '', '', 10),
 ('bx_files_per_page_profile', '6', @iCategId, '_bx_files_option_per_page_profile', 'digit', '', '', '', 12),
 ('bx_files_per_page_browse_showcase', '32', @iCategId, '_sys_option_per_page_browse_showcase', 'digit', '', '', '', 15),
@@ -177,7 +178,7 @@ INSERT INTO `sys_pages_blocks`(`object`, `cell_id`, `module`, `title`, `designbo
 SET @iPBCellProfile = 3;
 SET @iPBCellGroup = 4;
 INSERT INTO `sys_pages_blocks` (`object`, `cell_id`, `module`, `title_system`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `order`) VALUES
-('trigger_page_profile_view_entry', @iPBCellProfile, 'bx_files', '_bx_files_page_block_title_sys_my_entries', '_bx_files_page_block_title_my_entries', 11, 2147483647, 'service', 'a:3:{s:6:"module";s:8:"bx_files";s:6:"method";s:13:"browse_author";s:6:"params";a:2:{i:0;s:12:"{profile_id}";i:1;a:2:{s:8:"per_page";s:25:"bx_files_per_page_profile";s:13:"empty_message";b:0;}}}', 0, 0, 0);
+('trigger_page_profile_view_entry', @iPBCellProfile, 'bx_files', '_bx_files_page_block_title_sys_my_entries', '_bx_files_page_block_title_my_entries', 11, 2147483647, 'service', 'a:3:{s:6:"module";s:8:"bx_files";s:6:"method";s:13:"browse_author";s:6:"params";a:2:{i:0;s:12:"{profile_id}";i:1;a:3:{s:8:"per_page";s:25:"bx_files_per_page_profile";s:13:"empty_message";b:0;s:10:"no_toolbar";b:1;}}}', 0, 0, 0);
 
 -- PAGE: service blocks
 
@@ -260,13 +261,14 @@ INSERT INTO `sys_menu_sets`(`set_name`, `module`, `title`, `deletable`) VALUES
 ('bx_files_view_inline', 'bx_files', '_bx_files_menu_set_title_view_inline', 0);
 
 INSERT INTO `sys_menu_items`(`set_name`, `module`, `name`, `title_system`, `title`, `link`, `onclick`, `target`, `icon`, `submenu_object`, `visible_for_levels`, `active`, `copyable`, `order`) VALUES
-('bx_files_view_inline', 'bx_files', 'bookmark', '_bx_files_menu_item_title_system_bookmark', '_bx_files_menu_item_title_bookmark', '', '{js_object}.bookmark({content_id}, this); return false;', '', 'far star', '', 2147483647, 1, 0, 1),
-('bx_files_view_inline', 'bx_files', 'download-file', '_bx_files_menu_item_title_system_download_file', '_bx_files_menu_item_title_download_file', 'modules/?r=files/download/{file_download_token}/{content_id}.{file_ext}', '', '_blank', 'download', '', 2147483647, 1, 0, 2),
-('bx_files_view_inline', 'bx_files', 'delete-file-quick', '_bx_files_menu_item_title_system_delete_entry', '_bx_files_menu_item_title_delete_entry', 'page.php?i=delete-file&id={content_id}', '{js_object}.delete({content_id}); return false;', '', 'remove', '', 2147483647, 1, 0, 3),
-('bx_files_view_inline', 'bx_files', 'move-to', '_bx_files_menu_item_title_system_move_to_entry', '_bx_files_menu_item_title_move_to_entry', '', 'bx_alert(\'TODO\');return false;', '', 'file-export', '', 2147483647, 1, 0, 4),
-('bx_files_view_inline', 'bx_files', 'edit-title', '_bx_files_menu_item_title_system_edit_entry', '_bx_files_menu_item_title_edit_entry', 'page.php?i=edit-file&id={content_id}', '{js_object}.edit({content_id}); return false;', '', 'pencil-alt', '', 2147483647, 1, 0, 5),
-('bx_files_view_inline', 'bx_files', 'preview', '_bx_files_menu_item_title_system_info_entry', '_bx_files_menu_item_title_info_entry', '', '{js_object}.info({content_id}); return false;', '', 'info', '', 2147483647, 1, 0, 6),
-('bx_files_view_inline', 'bx_files', 'report', '_sys_menu_item_title_system_va_report', '', '', '', '', '', '', 2147483647, 1, 0, 7),
+('bx_files_view_inline', 'bx_files', 'favorite', '_sys_menu_item_title_system_va_favorite', '_sys_menu_item_title_va_favorite', '', '', '', '', '', 2147483647, 0, 0, 1),
+('bx_files_view_inline', 'bx_files', 'bookmark', '_bx_files_menu_item_title_system_bookmark', '_bx_files_menu_item_title_bookmark', '', '{js_object}.bookmark({content_id}, this); return false;', '', 'far star', '', 2147483647, 1, 0, 2),
+('bx_files_view_inline', 'bx_files', 'download-file', '_bx_files_menu_item_title_system_download_file', '_bx_files_menu_item_title_download_file', 'modules/?r=files/download/{file_download_token}/{content_id}.{file_ext}', '', '_blank', 'download', '', 2147483647, 1, 0, 3),
+('bx_files_view_inline', 'bx_files', 'delete-file-quick', '_bx_files_menu_item_title_system_delete_entry', '_bx_files_menu_item_title_delete_entry', 'page.php?i=delete-file&id={content_id}', '{js_object}.delete({content_id}); return false;', '', 'remove', '', 2147483647, 1, 0, 4),
+('bx_files_view_inline', 'bx_files', 'move-to', '_bx_files_menu_item_title_system_move_to_entry', '_bx_files_menu_item_title_move_to_entry', '', '{js_object}.moveTo({content_id}); return false;', '', 'file-export', '', 2147483647, 1, 0, 5),
+('bx_files_view_inline', 'bx_files', 'edit-title', '_bx_files_menu_item_title_system_edit_entry', '_bx_files_menu_item_title_edit_entry', 'page.php?i=edit-file&id={content_id}', '{js_object}.edit({content_id}); return false;', '', 'pencil-alt', '', 2147483647, 1, 0, 6),
+('bx_files_view_inline', 'bx_files', 'preview', '_bx_files_menu_item_title_system_info_entry', '_bx_files_menu_item_title_info_entry', '', '{js_object}.info({content_id}); return false;', '', 'info', '', 2147483647, 1, 0, 7),
+('bx_files_view_inline', 'bx_files', 'report', '_sys_menu_item_title_system_va_report', '', '', '', '', '', '', 2147483647, 1, 0, 8),
 ('bx_files_view_inline', 'bx_files', 'more-auto', '_sys_menu_item_title_system_va_more_auto', '_sys_menu_item_title_va_more_auto', 'javascript:void(0)', '', '', 'ellipsis-v', '', 2147483647, 1, 0, 9999);
 
 

+ 3 - 0
modules/boonex/files/install/sql/install.sql

@@ -30,10 +30,13 @@ CREATE TABLE IF NOT EXISTS `bx_files_main` (
   `allow_view_to` varchar(16) NOT NULL DEFAULT '3',
   `status` enum('active','hidden') NOT NULL DEFAULT 'active',
   `status_admin` enum('active','hidden') NOT NULL DEFAULT 'active',
+  `type` enum('file', 'folder') NOT NULL DEFAULT 'file',
+  `parent_folder_id` int(10) unsigned NOT NULL DEFAULT 0,
   PRIMARY KEY (`id`),
   FULLTEXT KEY `search_fields` (`title`,`desc`,`data`)
 );
 
+
 -- TABLE: storages & transcoders
 CREATE TABLE IF NOT EXISTS `bx_files_files` (
   `id` int(11) NOT NULL AUTO_INCREMENT,

+ 4 - 0
modules/boonex/files/js/menu_tools.js

@@ -45,4 +45,8 @@ BxFilesMenuTools.prototype.delete = function(iContentId) {
 	});
 };
 
+BxFilesMenuTools.prototype.moveTo = function(iContentId) {
+    $.post(this._sActionsUrl + 'move_files/', {file: iContentId}, processJsonData, 'json');
+}
+
 /** @} */

+ 99 - 4
modules/boonex/files/js/toolbar_tools.js

@@ -6,12 +6,42 @@ function BxFilesBrowserTools(oOptions) {
     this._sUniq = oOptions.unique_ident;
     this._aRequestParams = oOptions.aRequestParams;
     this._iSearchTimeoutId = 0;
+    this._sActionUrl = oOptions.sActionUrl;
+    this._iContext = oOptions.context;
+    this._iCurrentFolder = oOptions.current_folder;
+    this._iCurrentPage = oOptions.current_page;
 
     this._oToolbarObject = $('#bx-files-browser-toolbar-' + this._sUniq).get(0);
+
+    this.aSelectedFiles = [];
+
+    var $this = this;
+
+    $('input[type=checkbox]', $(this._oToolbarObject).parent()).change(function(){
+        $this.updateBulkActionsPanelVisivility();
+    });
+
+    $(window).on('files_browser.update', function(){
+        $this.reloadBlock($this._iCurrentPage);
+    });
 }
 
-BxFilesBrowserTools.prototype.reloadBlock = function() {
-    loadDynamicBlockAutoPaginate(this._oToolbarObject, '0', '0', this.getRequestUrl());
+BxFilesBrowserTools.prototype.findSelectedFiles = function() {
+    this.aSelectedFiles = [];
+    var $this = this;
+    $('input[type=checkbox]:checked', $(this._oToolbarObject).parent()).each(function(i, el){
+        if ($(el).val() > 0) $this.aSelectedFiles.push($(el).val());
+    });
+}
+
+BxFilesBrowserTools.prototype.updateBulkActionsPanelVisivility = function() {
+    this.findSelectedFiles();
+    if (this.aSelectedFiles.length) $('.bx-files-bulk-actions', this._oToolbarObject).slideDown();
+    else $('.bx-files-bulk-actions', this._oToolbarObject).slideUp();
+}
+
+BxFilesBrowserTools.prototype.reloadBlock = function(iPage) {
+    loadDynamicBlockAutoPaginate(this._oToolbarObject, iPage, '0', this.getRequestUrl());
 }
 
 BxFilesBrowserTools.prototype.onChangeFilter = function(oFilter) {
@@ -43,7 +73,8 @@ BxFilesBrowserTools.prototype.getRequestUrl = function() {
     return  this._aRequestParams.unit_view_param + '=' + this._sView + '&' +
             this._aRequestParams.bookmarks_param + '=' + this._bBookmarks + '&' +
             this._aRequestParams.sorting_param + '=' + this._sSorting + '&' +
-            this._aRequestParams.keyword_param + '=' + this._sKeyword;
+            this._aRequestParams.keyword_param + '=' + this._sKeyword + '&' +
+            this._aRequestParams.current_folder + '=' + this._iCurrentFolder;
 }
 
 BxFilesBrowserTools.prototype.onSelectAll = function(el) {
@@ -54,4 +85,68 @@ BxFilesBrowserTools.prototype.onSelectAll = function(el) {
         attr('checked', $(el).attr('checked') == 'checked').change(function(){
             $(el).attr('checked', false);
         });
-};
+};
+
+BxFilesBrowserTools.prototype.onBeforeUpload = function() {
+    $.get(this._sActionUrl + 'clear_ghosts/');
+}
+
+BxFilesBrowserTools.prototype.onAfterUpload = function() {
+    var $this = this;
+    $.post(this._sActionUrl + 'upload_completed/', {context: this._iContext, folder: this._iCurrentFolder}, function(oData) {
+        processJsonData(oData);
+        $this.reloadBlock();
+    }, 'json');
+}
+
+BxFilesBrowserTools.prototype.addFolder = function(sMessage) {
+    var $this = this;
+    bx_prompt(sMessage, '', function(oPopup) {
+        var name = oPopup.getValue();
+
+        $.post($this._sActionUrl + 'create_folder/', {context: $this._iContext, current_folder: $this._iCurrentFolder, name: name}, function (oData) {
+            processJsonData(oData);
+            $this.reloadBlock();
+        }, 'json');
+    });
+}
+
+BxFilesBrowserTools.prototype.folderNavigate = function(id) {
+    this._iCurrentFolder = id >= 0 ? id : -this._iCurrentFolder;
+    this.reloadBlock();
+}
+
+BxFilesBrowserTools.prototype.bookmarkFiles = function() {
+    if (!this.aSelectedFiles.length) return;
+
+    var $this = this;
+    $.post(this._sActionUrl+'bookmark/0', {bulk: this.aSelectedFiles}, function(oData){
+        processJsonData(oData);
+        $this.reloadBlock();
+    }, 'json');
+}
+
+BxFilesBrowserTools.prototype.deleteFiles = function() {
+    if (!this.aSelectedFiles.length) return;
+
+    var $this = this;
+
+    bx_confirm(_t('_Are_you_sure'), function(){
+		$.post($this._sActionUrl+'entry_delete/0', {bulk: $this.aSelectedFiles}, function(oData){
+            processJsonData(oData);
+            $this.reloadBlock();
+        }, 'json');
+	});
+}
+
+BxFilesBrowserTools.prototype.moveFiles = function() {
+    if (!this.aSelectedFiles.length) return;
+
+    $.post(this._sActionUrl + 'move_files/', {bulk: this.aSelectedFiles}, processJsonData, 'json');
+}
+
+BxFilesBrowserTools.prototype.downloadFiles = function() {
+    if (!this.aSelectedFiles.length) return;
+
+    window.open(this._sActionUrl + 'download/0/0/' + JSON.stringify(this.aSelectedFiles),'_blank');
+}

+ 12 - 5
modules/boonex/files/template/css/main.css

@@ -25,7 +25,7 @@
     white-space: nowrap;
 }
 
-.bx-files-text-unit-gallery .bx-base-general-unit-meta ul.bx-menu-custom.bx-menu-hor {
+.bx-files-text-unit-gallery .bx-base-general-unit-meta ul.bx-menu-custom.bx-menu-custom-hor {
     align-items: center;
     -webkit-align-items: center;
     justify-content: center;
@@ -39,7 +39,7 @@
 .bx-files-inline-menu ul.bx-menu-object-bx_files_view_inline li.bx-menu-item {
     white-space: nowrap;
 }
-.bx-files-text-unit-gallery .bx-menu-custom.bx-menu-hor.bx-menu-more-auto {
+.bx-files-text-unit-gallery .bx-menu-custom.bx-menu-custom-hor.bx-menu-more-auto {
     justify-content: center;
 }
 .bx-files-text-unit-gallery {
@@ -50,13 +50,13 @@
 .bx-files-text-unit-gallery .bx-base-text-unit-text {
     overflow: hidden;
 }
-.bx-media-desktop .bx-files-text-unit-gallery {
+.bx-media-desktop .bx-files-with-toolbar .bx-files-text-unit-gallery {
     max-width: 33.25%;
 }
-.bx-media-tablet2 .bx-files-text-unit-gallery {
+.bx-media-tablet2 .bx-files-with-toolbar .bx-files-text-unit-gallery {
     max-width: 49.8%;
 }
-.bx-media-phone2 .bx-files-text-unit-gallery {
+.bx-media-phone2 .bx-files-with-toolbar .bx-files-text-unit-gallery {
     max-width: 100%;
 }
 
@@ -117,6 +117,7 @@
 }
 .bx-files-text-unit-simple-row .bx-files-fileinfo i {
     font-size: 2.3rem;
+    line-height: 2.3rem;
 }
 .bx-files-text-unit-simple-row .bx-base-general-unit-meta ul {
     margin: 0px!important;
@@ -151,4 +152,10 @@
 .bx-files-toolbar-spacer {
     width: 100%;
     margin-bottom: 0.5rem;
+}
+.bx-files-folder-path i:last-child {
+    display: none;
+}
+.bx-files-bulk-actions {
+    display: none;
 }

+ 25 - 2
modules/boonex/files/template/files_browser_toolbar.html

@@ -1,8 +1,26 @@
 <div class="bx-def-padding-sec-leftright bx-def-border-bottom bx-files-border-thick bx-files-browser-toolbar bx-clearfix" id="bx-files-browser-toolbar-__unique_ident__">
+    <div class="bx-def-margin-bottom bx-files-folder-path">
+        <bx_repeat:folder_path>
+            <a href="#" onclick="__js_object__.folderNavigate(__folder__); return false;">__name__</a> <i class="sys-icon chevron-right"></i>
+        </bx_repeat:folder_path>
+    </div>
     <bx_if:upload_visible>
     <div class="bx-files-toolbar-controls-group">
-        <button class="bx-btn" onclick="bx_alert('TODO'); return false;"><i class="sys-icon upload"></i>&nbsp;Upload</button>
-        <button class="bx-btn" onclick="bx_alert('TODO'); return false;"><i class="sys-icon plus"></i>&nbsp;Add Folder</button>
+        __uploader_code__
+        <script>
+            $(document).ready(function(){
+                //catch onUploadCompleted call here
+                __uploader_js_object__.onUploadCompletedDefault = __uploader_js_object__.onUploadCompleted;
+                __uploader_js_object__.onUploadCompleted = function (sErrorMsg) {
+                    __uploader_js_object__.onUploadCompletedDefault(sErrorMsg);
+                    __js_object__.onAfterUpload();
+                }
+            });
+        </script>
+        <button class="bx-btn" onclick="__js_object__.onBeforeUpload(); __uploader_js_object__.showUploaderForm();"><i class="sys-icon upload"></i>&nbsp;<bx_text:_bx_files_txt_upload /></button>
+        <bx_if:create_folder_allowed>
+        <button class="bx-btn" onclick="__js_object__.addFolder('__folder_name_message__');"><i class="sys-icon plus"></i>&nbsp;<bx_text:_bx_files_txt_add_folder /></button>
+        </bx_if:create_folder_allowed>
     </div>
     </bx_if:upload_visible>
 
@@ -27,6 +45,11 @@
         __sorting_dropdown__
         __filter_box__
     </div>
+    <div class="bx-files-bulk-actions bx-def-margin-bottom">
+        <bx_repeat:bulk_actions>
+            <button class="bx-btn" onclick="__js_object__.__handler__();" title="__title__"><i class="sys-icon __icon__"></i></button>
+        </bx_repeat:bulk_actions>
+    </div>
 </div>
 
 <div class="bx-files-toolbar-spacer"></div>

+ 36 - 0
modules/boonex/files/template/folders_tree.html

@@ -0,0 +1,36 @@
+__js_css__
+
+<div class="bx-def-margin-bottom">
+    <div id="bx-files-dir-tree">__list__</div>
+</div>
+<button class="bx-btn" onclick="$('#bx-files-popup').dolPopupHide();"><bx_text:_sys_close /></button>
+<button class="bx-btn" onclick="" style="display: none;" id="bx-files-btn-move"><bx_text:_bx_files_txt_move_to /></button>
+<div class="bx-clear"></div>
+
+<script>
+
+    $(document).ready(function(){
+        $('#bx-files-dir-tree').jstree({
+            "core" : {
+                "multiple" : false
+            }
+        });
+
+        $('#bx-files-dir-tree').on('changed.jstree', function (e, data) {
+            if (data.selected.length) {
+                var sId = $(data.instance.get_node(data.selected)).attr('id');
+                var iId = parseInt(sId.replace('bx-files-folder-', ''));
+                $('#bx-files-btn-move').show().data('move_to', iId);
+            } else {
+                $('#bx-files-btn-move').hide();
+            }
+        });
+
+        $('#bx-files-btn-move').click(function(){
+            $.post('__actions_url__move_files', {bulk: __files__, move_to: $(this).data('move_to')}, function(oData) {
+                processJsonData(oData);
+                $('#bx-files-popup').dolPopupHide();
+            }, 'json');
+        });
+    });
+</script>

+ 12 - 0
modules/boonex/files/template/unit_simple_row_private.html

@@ -0,0 +1,12 @@
+<div class="__class__ bx-def-padding-sec bx-def-border-top bx-files-border-thick bx-files-text-unit-simple-row bx-file-entry bx-file-entry-private" file_id="__id__">
+    <div class="bx-files-fileinfo">
+        <input type="checkbox" class="bx-def-font-inputs bx-form-input-checkbox bx-def-margin-right" value="" disabled="disabled" />
+        <i class="sys-icon far file sys-colored bx-def-font-large bx-def-padding-sec-right col-gray"></i>
+        <div class="bx-files-fileinfo-text">
+            <div class="bx-files-filename"><bx_text:_sys_private /></div>
+            <div class="bx-base-text-unit-summary">
+                __summary__
+            </div>
+        </div>
+    </div>
+</div>