assign([ 'menu' => \Phpcmf\Service::M('auth')->_admin_menu( [ '附件管理' => [\Phpcmf\Service::L('Router')->class.'/index', 'fa fa-folder'], '已归档的附件' => [\Phpcmf\Service::L('Router')->class.'/db_index', 'fa fa-folder'], '未归档的附件' => [\Phpcmf\Service::L('Router')->class.'/unused_index', 'fa fa-folder-o'], '变更储存策略' => ['add:'.\Phpcmf\Service::L('Router')->class.'/remote_edit', 'fa fa-edit', '500px', '400px'], 'help' => [356], ] ) ]); } // 附件目录(资源管理器) public function index() { $rel = $this->_dir_rel_path(\Phpcmf\Service::L('input')->get('path')); $listing = $this->_dir_listing($rel); if ($listing === null) { $rel = ''; $listing = $this->_dir_listing($rel); } $p = [ 'size' => 9999, 'exts' => '*', 'count' => 20, 'attachment' => 0, 'image_reduce' => 0, 'chunk' => 10 * 1024 * 1024, ]; $upload = [ 'url' => dr_web_prefix(SELF.'?c='.\Phpcmf\Service::L('Router')->class.'&m=dir_upload') .'&siteid='.SITE_ID, 'param' => $p, ]; $uriprefix = trim(APP_DIR.'/'.\Phpcmf\Service::L('Router')->class, '/'); $parent_rel = ''; if ($listing['rel'] !== '') { $norm = str_replace('\\', '/', $listing['rel']); $d = dirname($norm); $parent_rel = ($d === '.' || $d === '') ? '' : str_replace('\\', '/', $d); } \Phpcmf\Service::V()->assign([ 'dir' => IS_DEV ? SYS_UPLOAD_PATH : dr_safe_replace_path(SYS_UPLOAD_PATH), 'url' => SYS_UPLOAD_URL, 'rel' => $listing['rel'], 'parent_rel' => $parent_rel, 'dirs' => $listing['dirs'], 'files' => $listing['files'], 'breadcrumb' => $this->_dir_breadcrumb($listing['rel']), 'upload' => $upload, 'uriprefix' => $uriprefix, 'dir_delete_url' => dr_url($uriprefix.'/dir_delete'), 'dir_mkdir_url' => dr_url($uriprefix.'/dir_mkdir'), 'dir_replace_url' => dr_url($uriprefix.'/dir_replace'), 'dir_folder_size_url' => dr_url($uriprefix.'/dir_folder_size'), 'dir_js' => [ 'confirm_del' => addslashes(dr_lang('你确定要删除它们吗?')), 'mkdir_title' => addslashes(dr_lang('新建文件夹')), 'name_empty' => addslashes(dr_lang('名称不能为空')), 'upload_fail' => addslashes(dr_lang('上传失败')), ], 'meta_title' => dr_lang('附件目录'), ]); \Phpcmf\Service::V()->display('attachment_dir.html'); } /** * AJAX:递归统计文件夹及子目录文件总大小 */ public function dir_folder_size() { if (function_exists('set_time_limit')) { @set_time_limit(300); } $rel = $this->_dir_rel_path(\Phpcmf\Service::L('input')->get('path')); if ($rel === '') { $this->_json(0, dr_lang('参数错误')); } list($ok, $abs, $r) = $this->_dir_safe($rel); if (!$ok || !is_dir($abs)) { $this->_json(0, dr_lang('目录不存在')); } $root = rtrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, SYS_UPLOAD_PATH), DIRECTORY_SEPARATOR); $rootReal = realpath($root); if ($rootReal === false) { $this->_json(0, dr_lang('目录无效或不存在')); } $bytes = $this->_dir_tree_size($abs, $rootReal); $this->_json(1, 'ok', [ 'size' => $bytes, 'size_text' => dr_format_file_size($bytes), ]); } /** * 递归统计目录下所有文件字节(不跟随符号链接) */ protected function _dir_tree_size($absDir, $rootReal) { $total = 0; if (!is_dir($absDir)) { return 0; } $absDir = rtrim($absDir, DIRECTORY_SEPARATOR); if (is_link($absDir)) { return 0; } if ($fp = @opendir($absDir)) { while (($name = readdir($fp)) !== false) { if ($name === '.' || $name === '..') { continue; } $sub = $absDir.DIRECTORY_SEPARATOR.$name; if (is_link($sub)) { continue; } if (is_file($sub)) { $sz = @filesize($sub); if ($sz !== false) { $total += $sz; } } elseif (is_dir($sub)) { $rp = realpath($sub); if ($rp !== false && $this->_dir_under_root($rootReal, $rp)) { $total += $this->_dir_tree_size($rp, $rootReal); } } } closedir($fp); } return $total; } /** * 附件目录内上传(写入当前文件夹,并走附件归档) */ public function dir_upload() { if (!IS_POST) { $this->_json(0, dr_lang('请求错误')); } $rel = $this->_dir_rel_path(\Phpcmf\Service::L('input')->post('path')); list($ok, $abs, $r) = $this->_dir_safe($rel); if (!$ok || !is_dir($abs)) { $this->_json(0, dr_lang('目录无效或不存在')); } $cfg = [ 'form_name' => 'file_data', 'file_exts' => ['*'], 'file_size' => 9999 * 1024 * 1024, 'attachment' => \Phpcmf\Service::M('Attachment')->get_attach_info(0), 'watermark' => 0, 'save_name' => 'null', ]; if ($r !== '') { $cfg['path'] = $r; } $rt = \Phpcmf\Service::L('upload')->upload_file($cfg); if (!$rt['code']) { exit(dr_array2string($rt)); } $data = []; if (defined('SYS_ATTACHMENT_CF') && SYS_ATTACHMENT_CF && $rt['data']['md5']) { $att = \Phpcmf\Service::M()->table('attachment') ->where('uid', $this->uid) ->where('filemd5', $rt['data']['md5']) ->where('fileext', $rt['data']['ext']) ->where('filesize', $rt['data']['size']) ->getRow(); if ($att) { $storage = new \Phpcmf\Library\Storage($this); $storage->delete(\Phpcmf\Service::M('Attachment')->get_attach_info(0), $rt['data']['file']); $rt['data'] = $this->get_attachment($att['id']); if ($rt['data']) { $rt['data']['name'] = $rt['data']['filename']; } $data = dr_return_data($att['id'], 'ok'); } } if (!$data) { $data = \Phpcmf\Service::M('Attachment')->save_data($rt['data']); if (!$data['code']) { exit(dr_array2string($data)); } } exit(dr_array2string(['code' => 1, 'msg' => dr_lang('上传成功'), 'id' => $data['code'], 'info' => $rt['data']])); } /** * 覆盖重传指定文件(扩展名须一致) */ public function dir_replace() { if (!IS_POST) { $this->_json(0, dr_lang('请求错误')); } $file = isset($_FILES['file_data']) ? $_FILES['file_data'] : null; $maxBytes = 9999 * 1024 * 1024; if ($file && !empty($file['size']) && $file['size'] > $maxBytes) { $this->_json(0, dr_lang('文件大小超出网站限制')); } $rel = $this->_dir_rel_path(\Phpcmf\Service::L('input')->post('replace')); if ($rel === '') { $this->_json(0, dr_lang('参数错误')); } list($ok, $abs, $r) = $this->_dir_safe($rel); if (!$ok || !is_file($abs)) { $this->_json(0, dr_lang('文件不存在')); } $name = basename(str_replace('\\', '/', $abs)); $oldExt = strpos($name, '.') !== false ? strtolower(trim(strrchr($name, '.'), '.')) : ''; $cfg = [ 'form_name' => 'file_data', 'file_exts' => $oldExt !== '' ? [$oldExt] : ['*'], 'file_name' => $abs, ]; $rt = \Phpcmf\Service::L('upload')->update_file($cfg); if (!$rt['code']) { exit(dr_array2string($rt)); } exit(dr_array2string(['code' => 1, 'msg' => dr_lang('上传成功')])); } /** * 删除所选文件或空目录 */ public function dir_delete() { if (!IS_POST) { $this->_json(0, dr_lang('请求错误')); } $paths = \Phpcmf\Service::L('input')->post('paths'); if (!$paths || !is_array($paths)) { $this->_json(0, dr_lang('你还没有选择呢')); } foreach ($paths as $p) { $rel = $this->_dir_rel_path($p); if ($rel === '') { $this->_json(0, dr_lang('禁止删除附件根目录')); } list($ok, $abs, $r) = $this->_dir_safe($rel); if (!$ok) { continue; } if (is_file($abs)) { if (!@unlink($abs)) { $this->_json(0, dr_lang('文件删除失败')); } } elseif (is_dir($abs)) { $items = @scandir($abs); $left = $items ? array_diff($items, ['.', '..']) : []; if ($left) { $this->_json(0, dr_lang('目录非空,无法删除')); } if (!@rmdir($abs)) { $this->_json(0, dr_lang('目录删除失败')); } } } $this->_json(1, dr_lang('操作成功')); } /** * 在当前目录下新建文件夹 */ public function dir_mkdir() { if (!IS_POST) { $this->_json(0, dr_lang('请求错误')); } $rel = $this->_dir_rel_path(\Phpcmf\Service::L('input')->post('path')); $name = trim((string) \Phpcmf\Service::L('input')->post('name')); $name = dr_safe_filename($name); if ($name === '' || strpos($name, '.') === 0) { $this->_json(0, dr_lang('名称不规范')); } list($ok, $parentAbs, $r) = $this->_dir_safe($rel); if (!$ok || !is_dir($parentAbs)) { $this->_json(0, dr_lang('当前目录无效')); } $newAbs = rtrim($parentAbs, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$name; if (file_exists($newAbs)) { $this->_json(0, dr_lang('已存在同名文件或目录')); } if (!dr_mkdirs($newAbs)) { $this->_json(0, dr_lang('目录创建失败')); } $this->_json(1, dr_lang('操作成功')); } /** * 规范化相对路径(相对 SYS_UPLOAD_PATH) * 按段解析,禁止路径跳出上传根(..),避免简单替换字符串被绕过 */ protected function _dir_rel_path($raw) { $raw = (string) $raw; if ($raw === '' || strpos($raw, "\0") !== false) { return ''; } $raw = str_replace('\\', '/', $raw); $raw = str_replace(["\r", "\n", '<', '>', '{', '}'], '', $raw); $raw = trim($raw, '/'); if ($raw === '') { return ''; } $parts = explode('/', $raw); $out = []; foreach ($parts as $p) { if ($p === '' || $p === '.') { continue; } if ($p === '..') { if (empty($out)) { return ''; } array_pop($out); continue; } $p = trim($p); if ($p === '' || $p === '.' || $p === '..') { continue; } $out[] = $p; } return implode('/', $out); } /** * 是否位于上传根目录之下 */ protected function _dir_under_root($rootReal, $candidate) { $root = strtolower(str_replace('\\', '/', rtrim($rootReal, '/\\'))); $path = strtolower(str_replace('\\', '/', $candidate)); return $path === $root || strpos($path, $root.'/') === 0; } /** * 解析安全绝对路径 */ protected function _dir_safe($rel) { $rel = $this->_dir_rel_path($rel); $root = rtrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, SYS_UPLOAD_PATH), DIRECTORY_SEPARATOR); if (!is_dir($root)) { return [false, '', '']; } $rootReal = realpath($root); if ($rootReal === false) { return [false, '', '']; } if ($rel === '') { return [true, $rootReal.DIRECTORY_SEPARATOR, '']; } $sub = str_replace('/', DIRECTORY_SEPARATOR, $rel); $full = $rootReal.DIRECTORY_SEPARATOR.$sub; if (is_file($full) || is_dir($full)) { $rp = realpath($full); if ($rp !== false && $this->_dir_under_root($rootReal, $rp)) { return [true, $rp, $rel]; } return [false, '', '']; } return [false, '', '']; } /** * 列出目录内容 */ protected function _dir_listing($rel) { list($ok, $abs, $r) = $this->_dir_safe($rel); if (!$ok || !is_dir($abs)) { return null; } $dirs = []; $files = []; if ($fp = @opendir($abs)) { while (($name = readdir($fp)) !== false) { if ($name === '.' || $name === '..' || $name === '.DS_Store') { continue; } if (strtolower(substr($name, -4)) === '.php') { continue; } $full = $abs.DIRECTORY_SEPARATOR.$name; $childRel = $r === '' ? $name : $r.'/'.$name; if (is_dir($full)) { $dirs[] = [ 'name' => $name, 'path' => $childRel, 'path_esc' => htmlspecialchars($childRel, ENT_QUOTES, 'UTF-8'), 'mtime' => filemtime($full), ]; } elseif (is_file($full)) { $ext = strtolower(trim(strrchr($name, '.'), '.')); $files[] = [ 'name' => $name, 'path' => $childRel, 'path_esc' => htmlspecialchars($childRel, ENT_QUOTES, 'UTF-8'), 'size' => filesize($full), 'mtime' => filemtime($full), 'ext' => $ext, 'url' => SYS_UPLOAD_URL.str_replace('\\', '/', $childRel), 'is_image' => dr_is_image($ext), 'is_mp4' => $ext === 'mp4', ]; } } closedir($fp); } usort($dirs, function ($a, $b) { return strcasecmp($a['name'], $b['name']); }); usort($files, function ($a, $b) { return strcasecmp($a['name'], $b['name']); }); return [ 'rel' => $r, 'dirs' => $dirs, 'files' => $files, ]; } /** * 面包屑 */ protected function _dir_breadcrumb($rel) { $crumbs = [['name' => dr_lang('附件根目录'), 'path' => '']]; if ($rel === '') { return $crumbs; } $parts = explode('/', $rel); $acc = ''; foreach ($parts as $p) { if ($p === '') { continue; } $acc = $acc === '' ? $p : $acc.'/'.$p; $crumbs[] = ['name' => $p, 'path' => $acc]; } return $crumbs; } // 已归档管理 public function db_index() { $field = [ 'uid' => [ 'ismain' => 1, 'fieldtype' => 'Text', 'fieldname' => 'uid', 'name' => '账号', ], 'related' => [ 'ismain' => 1, 'fieldtype' => 'Text', 'fieldname' => 'related', 'name' => dr_lang('附件归属'), ], 'fileext' => [ 'ismain' => 1, 'fieldtype' => 'Text', 'fieldname' => 'fileext', 'name' => dr_lang('扩展名'), ], 'filename' => [ 'ismain' => 1, 'fieldtype' => 'Text', 'fieldname' => 'filename', 'name' => dr_lang('附件名称'), ], 'attachment' => [ 'ismain' => 1, 'fieldtype' => 'Text', 'fieldname' => 'attachment', 'name' => dr_lang('附件路径'), ], ]; $remote = (int)$_GET['remote']; $where_list = 'id in (select id from '.\Phpcmf\Service::M()->dbprefix('attachment').' where siteid='.SITE_ID.')'; $remote && $where_list.= ' and `remote`='.$remote; $this->_init([ 'table' => 'attachment_data', 'field' => $field, 'order_by' => 'id desc', 'where_list' => $where_list, 'date_field' => 'inputtime', ]); $this->_List(); // 快捷上传字段参数 $p = [ 'size' => 9999, 'exts' => '*', 'count' => 20, 'attachment' => $remote, 'image_reduce' => 0, 'chunk' => 10 * 1024 * 1024, ]; $upload = [ 'url' => dr_web_prefix(SELF.'?c=api&token='.dr_get_csrf_token()) .'&siteid='.SITE_ID.'&m=upload&p='.dr_authcode($p, 'ENCODE'), 'param' => $p, 'back' => dr_now_url(), ]; \Phpcmf\Service::V()->assign([ 'field' => $field, 'table' => 'data', 'remote' => \Phpcmf\Service::M()->table('attachment_remote')->getAll(0, 'id'), 'upload' => $upload, ]); \Phpcmf\Service::V()->display('attachment_admin.html'); } // 未归档的附件 public function unused_index() { $field = [ 'author' => [ 'ismain' => 1, 'fieldtype' => 'Text', 'fieldname' => 'author', 'name' => dr_lang('账号'), ], 'fileext' => [ 'ismain' => 1, 'fieldtype' => 'Text', 'fieldname' => 'fileext', 'name' => dr_lang('扩展名'), ], 'uid' => [ 'ismain' => 1, 'fieldtype' => 'Text', 'fieldname' => 'uid', 'name' => 'uid', ], ]; $remote = (int)$_GET['remote']; $where_list = 'siteid='.SITE_ID; $remote && $where_list.= ' and `remote`='.$remote; $this->_init([ 'table' => 'attachment_unused', 'field' => $field, 'order_by' => 'id desc', 'where_list' => $where_list, 'date_field' => 'inputtime', ]); $this->_List(); \Phpcmf\Service::V()->assign([ 'field' => $field, 'table' => 'unused', 'remote' => \Phpcmf\Service::M()->table('attachment_remote')->getAll(0, 'id'), ]); \Phpcmf\Service::V()->display('attachment_admin.html'); } public function remote_edit() { if (IS_POST) { $post = \Phpcmf\Service::L('input')->post('data'); if ($post['o'] == $post['n']) { $this->_json(0, dr_lang('储存策略不能相同')); } \Phpcmf\Service::M()->table('attachment_unused')->where('remote', intval($post['o']))->update(0, [ 'remote' => intval($post['n']) ]); \Phpcmf\Service::M()->table('attachment_data')->where('remote', intval($post['o']))->update(0, [ 'remote' => intval($post['n']) ]); dr_dir_delete(WRITEPATH.'attach'); dr_mkdirs(WRITEPATH.'attach'); $this->_json(1, dr_lang('操作成功')); } \Phpcmf\Service::V()->assign([ 'form' => dr_form_hidden(), 'remote' => \Phpcmf\Service::M()->table('attachment_remote')->getAll(0, 'id'), ]); \Phpcmf\Service::V()->display('attachment_remote_edit.html'); } public function del() { $ids = \Phpcmf\Service::L('input')->get_post_ids(); if (!$ids) { $this->_json(0, dr_lang('你还没有选择呢')); } $table = \Phpcmf\Service::L('input')->post('table'); $table != 'data' && $table = 'unused'; $data = \Phpcmf\Service::M()->db->table('attachment_'.$table)->whereIn('id', $ids)->get()->getResultArray(); if (!$data) { $this->_json(0, dr_lang('所选附件不存在')); } foreach ($data as $t) { $rt = \Phpcmf\Service::M('attachment')->_delete_file($t); if (!$rt['code']) { return dr_return_data(0, $rt['msg']); } } $this->_json(1, dr_lang('操作成功')); } // 强制归档 public function edit() { $ids = \Phpcmf\Service::L('input')->get_post_ids(); if (!$ids) { $this->_json(0, dr_lang('你还没有选择呢')); } $data = \Phpcmf\Service::M()->db->table('attachment_unused')->whereIn('id', $ids)->get()->getResultArray(); if (!$data) { $this->_json(0, dr_lang('所选附件不存在')); } $related = 'Save'; foreach ($data as $t) { // 更新主索引表 \Phpcmf\Service::M()->table('attachment')->update($t['id'], array( 'related' => $related )); \Phpcmf\Service::M()->table('attachment_data')->insert(array( 'id' => $t['id'], 'uid' => $t['uid'], 'remote' => $t['remote'], 'author' => $t['author'], 'related' => $related, 'fileext' => $t['fileext'], 'filesize' => $t['filesize'], 'filename' => $t['filename'], 'inputtime' => $t['inputtime'], 'attachment' => $t['attachment'], 'attachinfo' => '', )); // 删除未归档附件 \Phpcmf\Service::M()->table('attachment_unused')->delete($t['id']); } $this->_json(1, dr_lang('操作成功')); } // 变更储存策略 public function type_edit() { $ids = \Phpcmf\Service::L('input')->get_post_ids(); if (!$ids) { $this->_json(0, dr_lang('你还没有选择呢')); } $rid = intval(\Phpcmf\Service::L('input')->post('remote')); if ($rid < 0) { $this->_json(0, dr_lang('你还没有选择储存策略')); } $table = \Phpcmf\Service::L('input')->post('table'); $table != 'data' && $table = 'unused'; \Phpcmf\Service::M()->table('attachment_'.$table)->where_in('id', $ids)->update(0, [ 'remote' => $rid ]); dr_dir_delete(WRITEPATH.'attach'); dr_mkdirs(WRITEPATH.'attach'); $this->_json(1, dr_lang('操作成功')); } // 重新上传附件 public function file_edit() { $id = \Phpcmf\Service::L('input')->get('id'); if (!$id) { $this->_json(0, dr_lang('你还没有选择呢')); } $data = $this->get_attachment($id, true); if (!$data) { $this->_json(0, dr_lang('附件信息不存在')); } \Phpcmf\Service::V()->assign([ 'data' => $data, ]); \Phpcmf\Service::V()->display('attachment_upload.html'); } // 重新上传附件 public function upload_edit() { $id = \Phpcmf\Service::L('input')->get('id'); if (!$id) { $this->_json(0, dr_lang('你还没有选择呢')); } $data = $this->get_attachment($id, true); if (!$data) { $this->_json(0, dr_lang('附件信息不存在')); } $rt = \Phpcmf\Service::L('upload')->upload_file([ 'save_name' => str_replace('.'.$data['fileext'], '', basename($data['attachment'])), 'path' => dirname($data['attachment']), 'form_name' => 'file_data', 'file_exts' => [$data['fileext']], 'file_size' => 1000 * 1024 * 1024, 'attachment' => \Phpcmf\Service::M('Attachment')->get_attach_info($data['remote']), ]); if (!$rt['code']) { exit(dr_array2string($rt)); } \Phpcmf\Service::M()->table('attachment_data')->update($id, ['filesize' => $rt['data']['size']]); \Phpcmf\Service::M()->table('attachment_unused')->update($id, ['filesize' => $rt['data']['size']]); $this->_json(1, dr_lang('上传成功'), $rt['data']); } }