CKFinder 3 – PHP 连接器文档
HOWTO

如果您没有找到您要查找的答案,请向我们发送您的问题: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&currentFolder=/&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 中您要检查的命令的 命令前事件(如果要检查配额:命令如 FileUploadCopyFilesImageResizeCreateFolder)。

有关实现此功能的完整源代码,请参阅 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&currentFolder=/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'
);
Files
└── dir2
└── dir3

或者指向更深层的子文件夹

$config['resourceTypes'][] = array(
'name' => 'Files',
'backend' => 'default',
'directory' => 'dir1/dir2'
);
Files
└── dir3

避免与 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>