如果您没有找到您要查找的答案,请向我们发送您的问题:https://cksource.com/contact
每个实例使用不同的文件夹
如果您使用多个 CKFinder 实例,您可以使用不同的 id
属性并将它们传递给服务器连接器请求。
CKFinder.start( { id: 'instanceNo1', pass: 'id' } );
CKFinder.start( { id: 'instanceNo2', pass: 'id' } );
在连接器端,您可以使用 $_GET['id']
获取当前实例的名称,并将其用于动态配置修改。这样,您可以让每个实例对本地文件系统后端使用其自己的根文件夹。
$config['backends'][] = array(
'name' => 'default',
'adapter' => 'local',
'baseUrl' => getBaseUrlByInstance($_GET['id']),
'root' => getRootByInstance($_GET['id'])
);
出于安全原因,您应该避免在目录路径中直接使用实例名称,并使用某种白名单。上面的配置示例中使用的 getRootByInstance()
函数可能如下所示
function getRootByInstance($instanceId)
{
$pathMap = array(
'instanceNo1' => '/instance/no1/path',
'instanceNo2' => '/instance/no2/path'
);
foreach ($pathMap as $id => $path) {
if ($id === $instanceId) {
return $path;
}
}
throw new \Exception('Invalid instance id', Error::CUSTOM_ERROR); // 如果没有传递有效的实例 ID。
}
每个用户私有文件夹
要为用户创建单独的目录,您需要创建一个简单的机制来将当前用户映射到适当的目录路径。
在构建目录路径时,您应该记住以下可能导致路径遍历攻击的内容
- 不要泄露任何敏感信息。
- 不要使用任何不安全的数据。
在此示例中,使用当前用户名 sha1
哈希。
$userDirectory = sha1($user->getUsername());
$config['backends'][] = array(
'name' => 'default',
'adapter' => 'local',
'baseUrl' => 'http://example.com/ckfinder/userfiles/' . $userDirectory,
'root' => '/path/to/ckfinder/userfiles/' . $userDirectory
);
注意:在为用户创建私有目录时,您还应该记住 CKFinder 内部目录,它们也应该分开。
$config['privateDir'] = array(
'backend' => 'default',
'tags' => '/path/to/ckfinder/private/dir/.ckfinder/' . $userDirectory . '/tags',
'logs' => '/path/to/ckfinder/private/dir/.ckfinder/' . $userDirectory . '/logs',
'cache' => '/path/to/ckfinder/private/dir/.ckfinder/' . $userDirectory . '/cache',
'thumbs' => '/path/to/ckfinder/private/dir/.ckfinder/' . $userDirectory . '/cache/thumbs',
);
将文件存储在文档根目录之外
在某些情况下,您可能希望将 CKFinder 用户的文件存储在不能直接从 Web 访问的文件夹中。您可以通过使用 root
选项为后端设置显式文件夹路径来做到这一点
$config['backends'][] = array(
'name' => 'default',
'adapter' => 'local',
'root' => '/my/private/directory/path',
'baseUrl' => '/download.php?path='
);
在上面的示例中,baseUrl
选项也被修改了,因此 CKFinder 中的当前文件路径将被附加为 path
参数。这样,仍然可以构建指向文件的 URL。对文件的访问由 PHP 脚本 download.php
管理,该脚本可以执行其他操作,例如日志记录、身份验证和安全检查。
或者,您可以使用 CKFinder 3.1.0 版本中引入的 Proxy 命令
$config['backends'][] = array(
'name' => 'default',
'adapter' => 'local',
'root' => '/my/private/directory/path',
'useProxyCommand' => true
);
使用上述配置,所有指向文件的链接都将指向 Proxy
命令,文件将由 PHP 连接器提供服务。
如果启用了 useProxyCommand
选项,则指向文件的示例链接
/ckfinder/core/connector/php/connector.php?command=Proxy&type=Files¤tFolder=/&fileName=foo.jpg
注意:如果您决定使用 useProxyCommand
选项,则 CKFinder 生成的所有链接都将依赖于连接器才能正常工作。
保护用户文件文件夹
在 Apache 上,您可以在所有上传文件将要存储的目标文件夹中禁用 PHP 引擎。如果您选择了不同的用户文件文件夹,请从其默认位置复制包含以下设置的 /ckfinder/userfiles/.htaccess
文件,并将其粘贴到目标文件夹中。
CKFinder 默认将私有文件(如缩略图、缓存、日志)存储在隐藏的 userfiles/.ckfinder
目录中。这些文件可能包含敏感数据,因此最佳实践是限制对其的访问。在 Apache 服务器上,私有文件夹的访问权限会因自动创建的 .htaccess
文件而被禁用。最佳做法是将 CKFinder 私有文件夹移出文档根目录,使其无法从 Web 访问。
您可以在 私有目录 配置选项中定义一个单独的后端,用于私有数据的 私有目录 配置选项。
$config['privateDir'] = array(
'backend' => 'private_files',
'tags' => './ckfinder/tags',
'logs' => './ckfinder/logs',
'cache' => './ckfinder/cache',
'thumbs' => './ckfinder/cache/thumbs',
);
$config['backends'][] = array(
'name' => 'private_files',
'adapter' => 'local',
'root' => '/private/files/path',
'chmodFiles' => 0755,
'chmodFolders' => 0755,
);
磁盘配额
在此示例中,假设您已使用 isQuotaAvailable()
方法实现了您自己的用户存储配额检查逻辑。您可以将此逻辑附加到 CKFinder 中您要检查的命令的 命令前事件(如果要检查配额:命令如 FileUpload
、CopyFiles
、ImageResize
、CreateFolder
)。
有关实现此功能的完整源代码,请参阅 DiskQuota 插件示例。
记录用户操作
在此示例中,目标是创建一个插件来将用户操作记录到文件中。这可以使用 事件 系统来实现。为了便于说明,假设所有对应于 中间事件 的用户操作都应记录。为此,需要创建一个简单的事件监听器并将其附加到应记录的事件。
有关实现此功能的完整源代码,请参阅 UserActionsLogger 插件示例。
注意:UserActionsLogger 插件只是一个示例。在实际应用程序中,您应该记住处理多个脚本对文件的并发访问,即您应该使用文件锁定。
如果插件注册正确,您应该在日志文件中看到类似以下内容的输出。
[2015.02.13 13:22:54] - dummyUser1 : ckfinder.createFolder.create
[2015.02.13 13:22:59] - dummyUser1 : ckfinder.uploadFile.upload
[2015.02.13 13:22:59] - dummyUser1 : ckfinder.thumbnail.createThumbnail
[2015.02.13 13:23:04] - dummyUser1 : ckfinder.renameFile.rename
[2015.02.13 13:23:04] - dummyUser1 : ckfinder.thumbnail.createThumbnail
[2015.02.13 13:23:09] - dummyUser1 : ckfinder.moveFiles.move
[2015.02.13 13:23:10] - dummyUser1 : ckfinder.thumbnail.createThumbnail
[2015.02.13 13:23:14] - dummyUser1 : ckfinder.copyFiles.copy
[2015.02.13 13:23:16] - dummyUser1 : ckfinder.thumbnail.createThumbnail
[2015.02.13 13:23:20] - dummyUser1 : ckfinder.deleteFiles.delete
在上面的示例中,创建了一个通用监听器来记录有关事件的非常基本的信息。当特定事件被调度时,更具体的事件对象将作为监听器参数传递。它包含有关当前操作的更多信息,例如已删除文件的路径、已上传的文件内容等。有关特定事件传递的事件对象参数类型的更多详细信息,请参阅 事件 部分。
自定义命令
此示例展示了一个简单的命令插件,它返回有关文件的基本信息。
有关实现此功能的完整源代码,请参阅 GetFileInfo 插件示例。
如果启用了此插件,您可以调用额外的 GetFileInfo
命令,该命令返回有关文件的某些非常基本的信息,例如大小和最后修改时间戳。此行为可以简单地更改为返回有关文件的任何其他信息(例如图像的 EXIF 数据或 mp3 文件的 ID3 标记)。
GetFileInfo
描述 | 返回有关文件的基本信息。 |
方法 | GET |
示例请求 | 获取 Files 资源类型 sub1 目录中 foo.png 文件的基本信息。/ckfinder/core/connector/php/connector.php?command=GetFileInfo&type=Files¤tFolder=/sub1/&fileName=foo.png
|
示例响应 | {
"resourceType": "Files",
"currentFolder": {
"path": "/sub1/",
"url": "/ckfinder/userfiles/files/sub1/",
"acl": 255
},
"type": "file",
"path":"files\/sub1\/1.png",
"timestamp":1425909932,
"size":1336
}
|
备注 | 上面的响应还附加了有关资源类型和当前文件夹的附加信息,这是 CKFinder JSON 响应的默认行为。您可以通过调用以下命令来禁用它 $workingFolder->omitResponseInfo();
另一种解决方案是直接从 execute 方法返回任何其他类型的 Response 对象。
|
有关命令的更多详细信息,请参阅 CKFinder PHP 连接器文档的 命令 部分。
将资源类型指向现有文件夹
资源类型文件夹可以使用 directory
配置选项定义(参见 resourceTypes)。定义的目录相对于后端的根目录。
考虑以下文件夹结构
rootDir
└── dir1
└── dir2
└── dir3
其中 rootDir
是为名为 default
的后端定义的根目录。
资源类型可以通过不提供 directory
配置选项来附加到根文件夹
$config['resourceTypes'][] = array(
'name' => 'Files',
'backend' => 'default'
);
使用上述配置,您将在 CKFinder 中看到以下文件夹树
Files
└── dir1
└── dir2
└── dir3
使用 directory
选项,您可以将资源类型指向任何子文件夹,如下所示
$config['resourceTypes'][] = array(
'name' => 'Files',
'backend' => 'default',
'directory' => 'dir1'
);
或者指向更深层的子文件夹
$config['resourceTypes'][] = array(
'name' => 'Files',
'backend' => 'default',
'directory' => 'dir1/dir2'
);
避免与 PHP 会话相关的性能问题
默认情况下,PHP 会话机制使用普通文件来保存会话数据。当请求发送到启动会话的 PHP 脚本时 session_start(),它会锁定会话文件。这意味着使用相同会话的任何并发请求都将保持挂起状态,直到之前请求所需的处理完成。
当其中一个请求非常耗时并阻塞了许多处理速度相对较快的请求时,这尤其不可取。为了避免这个问题,PHP 连接器会尽快关闭对会话的写入访问 session_write_close(),以便可以同时处理请求(参见 sessionWriteClose)。
PHP 会话也可能导致浏览器中内容缓存出现问题,这可能会降低应用程序速度并降低用户体验。
会话添加的缓存头取决于 session_cache_limiter() 配置。session_cache_limiter()
函数需要在 session_start()
之前调用。
由于 CKFinder 连接器不控制会话启动的时间,因此需要在主应用程序中进行配置。可以通过将空字符串作为 session_cache_limiter()
函数的参数提供,来关闭自动发送缓存头,如下所示
session_cache_limiter('');
在后端适配器中添加缓存层
在任何后端适配器类型中添加缓存层都是一项相当简单的任务。CKFinder PHP 连接器在后台使用 Flysystem 抽象层,因此有 很多 可供使用的缓存适配器,可以用来装饰常规的后端适配器。
请查看以下 GitHub 上的工单,以了解在 S3 后端实现 Redis 缓存的示例。
添加对自定义存储的支持
此示例演示了一个插件,它添加了对将文件存储在数据库中的支持。
出于本教程的目的,让我们假设文件将存储在一个数据库表中,该表的 SQL 架构如下所示
MySQL
CREATE TABLE files (
id int(11) NOT NULL AUTO_INCREMENT,
path varchar(255) NOT NULL,
type enum('file','dir') NOT NULL,
contents longblob,
size int(11) NOT NULL DEFAULT 0,
mimetype varchar(127),
timestamp int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY path_unique (path)
);
SQLite
CREATE TABLE files (
id INTEGER PRIMARY KEY,
path TEXT NOT NULL UNIQUE,
type TEXT NOT NULL,
contents BLOB,
size INTEGER NOT NULL DEFAULT 0,
mimetype TEXT,
timestamp INTEGER NOT NULL DEFAULT 0
);
实现自定义 Flysystem 适配器
CKFinder 3 PHP 服务器端连接器使用 Flysystem 作为文件系统抽象层。Flysystem 提供了一种非常方便的方式,可以使用公共 API 与各种文件系统进行通信,并且允许插入适配器,这些适配器可用于与任何类型的自定义存储进行通信。要熟悉 Flysystem 适配器的概念,请查看 "创建适配器" 这篇官方 Flysystem 文档中的文章。
在 CKFinder 3 PHP 连接器中添加自定义存储的第一步是创建 League\Flysystem\FilesystemAdapter 的实现。此接口定义了与给定文件系统进行通信所需的所有方法 - 例如写入、读取或删除文件。
查看 League\Flysystem\FilesystemAdapter 的自定义实现,该实现需要将文件保存到具有假设架构的数据库表中。 PDOAdapter 类使用 PDO PHP 扩展,它定义了在 PHP 中访问许多数据库系统的接口。PDOAdapter
类的构造函数接受两个参数:一个有效的 PDO
对象和一个应存储文件的表名。PDOAdapter
类的实例化如下所示。
MySQL
// https://secure.php.net/manual/en/ref.pdo-mysql.connection.php
$pdo = new PDO('mysql:host=hostname;dbname=database_name', 'username', 'password');
$adapter = new PDOAdapter($pdo, 'files');
SQLite
// https://secure.php.net/manual/en/ref.pdo-sqlite.connection.php
$pdo = new PDO('sqlite:/absolute/path/to/database.sqlite');
$adapter = new PDOAdapter($pdo, 'files');
使用插件注册自定义适配器
有了 League\Flysystem\FilesystemAdapter 的实现,现在该告诉 CKFinder 连接器使用它了。最方便的方法是创建一个连接器插件(见 插件开发),以便可以公开插件选项并在连接器 配置 中进行配置。以下示例演示了一个连接器插件,它注册了在上一步中实现的 PDOAdapter
。
namespace CKSource\CKFinder\Plugin\DatabaseAdapter;
// 如果自动加载器可以从 CKFinder 插件目录加载,则可能不需要此行。
require_once __DIR__.'/PDOAdapter.php';
use CKSource\CKFinder\CKFinder;
use CKSource\CKFinder\Plugin\PluginInterface;
use PDO;
class DatabaseAdapter implements PluginInterface
{
/**
* 将 DI 容器注入到插件中。
*/
public function setContainer(CKFinder $app)
{
$backendFactory = $app->getBackendFactory();
// 注册名为“database”的后端适配器。
$backendFactory->registerAdapter('database', function ($backendConfig) use ($backendFactory) {
// 使用 CKFinder 配置中定义的后端选项,创建 PDOAdapter 的实例。
$pdo = new PDO($backendConfig['dsn'], $backendConfig['username'], $backendConfig['password']);
$adapter = new PDOAdapter($pdo, $backendConfig['tableName']);
// 创建并返回 CKFinder 后端实例。
return $backendFactory->createBackend($backendConfig, $adapter);
});
}
/**
* 此插件有点特殊,因为它只使用后端配置选项。
* 此方法可以忽略,并简单地返回一个空数组。
*/
public function getDefaultConfig()
{
return [];
}
}
查看 DatabaseAdapter 插件示例,以获取实现此功能的完整源代码。
保护可公开访问的文件夹
在集成 CKFinder 时,您通常希望允许用户访问上传的文件,以便他们可以在编辑的内容中插入图像或指向文件的链接。这可以通过两种方式完成
- 您可以配置您的 CKFinder,以便使用 代理 命令通过连接器提供所有文件。
- 您可以使该文件夹可公开访问,以便所有文件都通过 Web 服务器提供。
如果您依赖 Web 服务器提供使用 CKFinder 上传的文件,则应采取额外的步骤,以确保文件以安全的方式提供。
让我们假设您已将 CKFinder 配置为允许上传 .avi
文件。
即使 .avi
文件随后使用有效的 Content-Type: video/x-msvideo
头提供,一些浏览器也可能会忽略此信息并对原始文件内容执行额外的检查。如果在文件内容中检测到任何类似 HTML 的数据,浏览器可能会决定忽略有关内容类型的消息,并将提供的內容视为普通网页。这种行为称为 "内容嗅探"(也称为“媒体类型嗅探”或“MIME 嗅探”),在某些情况下可能会导致安全问题(例如,它可能会为 XSS 攻击 打开大门)。
为了避免内容嗅探,您应该确保您的服务器在从可公开访问的文件夹提供文件时,将 X-Content-Type-Options: nosniff
头添加到所有 HTTP 响应中。X-Content-Type-Options
响应 HTTP 头是服务器使用的标记,用于指示 Content-Type
头设置的 MIME 类型不应更改,应遵循。因此,浏览器不会对接收到的内容执行任何内容嗅探。
Apache
如果您使用的是 Apache Web 服务器,则可以使用 mod_headers
添加自定义 HTTP 响应头。确保 mod_headers
模块已启用,并在可公开访问的文件夹的根目录(例如 userfiles/.htaccess
)中创建(或修改)以下 .htaccess
文件
Header set X-Content-Type-Options "nosniff"
Nginx
如果您使用的是 Nginx,则可以为每个位置定义自定义 HTTP 响应头
location /userfiles {
add_header X-Content-Type-Options nosniff;
}
Microsoft IIS
对于 Microsoft IIS 服务器,您可以在 web.config
文件中启用 X-Content-Type-Options
头
<system.webServer>
<httpProtocol>
<customHeaders>
<remove name="X-Content-Type-Options"/>
<add name="X-Content-Type-Options" value="nosniff"/>
</customHeaders>
</httpProtocol>
</system.webServer>