Install the Bundle
composer require champs-libres/wopi-bundle
Add to config/bundles.php:
return [
// ...
ChampsLibres\WopiBundle\WopiBundle::class => ['all' => true],
];
Configure WOPI Endpoints
Define routes in config/routes.yaml:
wopi:
resource: "@WopiBundle/Resources/config/routing.yaml"
prefix: /wopi
Implement WOPI Handler
Create a custom handler extending ChampsLibres\WopiBundle\Handler\WopiHandlerInterface:
use ChampsLibres\WopiBundle\Handler\WopiHandlerInterface;
use ChampsLibres\WopiBundle\Wopi\WopiRequest;
class CustomWopiHandler implements WopiHandlerInterface
{
public function checkLock(WopiRequest $request): bool { /* ... */ }
public function getFileInfo(WopiRequest $request): array { /* ... */ }
public function getFile(WopiRequest $request): string { /* ... */ }
// ... other required methods
}
Register Handler in Services
# config/services.yaml
services:
ChampsLibres\WopiBundle\Handler\WopiHandlerInterface:
class: App\Handler\CustomWopiHandler
Test with Collabora Online
Configure Collabora’s wopi.json to point to your /wopi endpoint and test file operations.
Use the getFileInfo method to return metadata (e.g., file size, MIME type) for Collabora to render previews:
public function getFileInfo(WopiRequest $request): array
{
$file = $this->storage->find($request->getFileId());
return [
'BaseFileName' => $file->getName(),
'Size' => $file->getSize(),
'Url' => $this->generateUrl('wopi_download', ['id' => $file->getId()]),
'UserId' => $request->getUserId(),
'UserCanWrite' => $request->getUserCanWrite(),
];
}
Incoming Requests
The bundle routes WOPI requests (e.g., GET /wopi/files/{fileId}) to your handler via the WopiHandlerInterface.
Example request object:
$request = new WopiRequest(
$fileId,
$userId,
$accessToken,
$userCanWrite,
$request->query->all()
);
Locking Mechanism
Implement checkLock() to validate concurrent edits:
public function checkLock(WopiRequest $request): bool
{
$lock = $this->lockService->getLock($request->getFileId());
return $lock->isActive() && $lock->getUserId() === $request->getUserId();
}
File Operations
getFile().
public function getFile(WopiRequest $request): string
{
return $this->storage->read($request->getFileId());
}
PUT requests in putFile().
public function putFile(WopiRequest $request, string $content): void
{
$this->storage->save($request->getFileId(), $content);
}
Permissions
Use getUserCanWrite() to enforce edit restrictions:
if (!$request->getUserCanWrite()) {
throw new \RuntimeException('User lacks write permissions.');
}
Storage Abstraction
Decouple storage logic by injecting a service (e.g., Filesystem, Doctrine ORM, or S3):
$this->storage = $container->get('your.storage.service');
Security
access_token against your auth system (e.g., JWT).fileId to prevent path traversal:
if (!preg_match('/^[a-f0-9]{32}$/', $fileId)) {
throw new \InvalidArgumentException('Invalid file ID.');
}
CORS Configuration Ensure Collabora’s domain is whitelisted in Symfony’s CORS settings:
# config/packages/nelmio_cors.yaml
nelmio_cors:
defaults:
allow_origin: ["https://your-collabora-domain.com"]
Logging Log WOPI actions for debugging:
$this->logger->info('WOPI action', [
'file_id' => $request->getFileId(),
'action' => $request->getAction(),
'user_id' => $request->getUserId(),
]);
Testing
Use WopiRequest in unit tests:
$request = new WopiRequest('file123', 'user456', 'token789', true, ['action' => 'getfileinfo']);
$this->assertEquals(['Size' => 1024], $handler->getFileInfo($request));
Missing Required Methods
The WopiHandlerInterface requires all methods (e.g., getLock, refreshLock). Omitting any will throw BadMethodCallException.
Fix: Implement a skeleton class:
class BaseWopiHandler implements WopiHandlerInterface {
public function getLock(WopiRequest $request) { /* ... */ }
// ... stub all methods
}
Incorrect File IDs
WOPI expects fileId to be a URL-encoded string. Decode it before use:
$fileId = rawurldecode($request->getFileId());
CORS Preflight Issues
Collabora sends OPTIONS requests before POST/PUT. Ensure your CORS middleware handles them:
nelmio_cors:
paths:
'^/wopi/':
methods: [GET, POST, PUT, OPTIONS]
allow_origin: ["*"] # Adjust in production!
Lock Expiry
Collabora expects locks to expire after inactivity. Implement refreshLock() to extend lock duration:
public function refreshLock(WopiRequest $request): void
{
$lock = $this->lockService->getLock($request->getFileId());
$lock->extend(300); // 5-minute extension
}
Large File Handling
For files >100MB, use chunked uploads (PUT with Content-Range headers). Validate ranges in putFile():
if (isset($_SERVER['HTTP_CONTENT_RANGE'])) {
$range = explode('-', $_SERVER['HTTP_CONTENT_RANGE']);
$this->storage->appendChunk($request->getFileId(), $range[0], $content);
}
Enable WOPI Debugging
Set WOPI_DEBUG in .env to log raw requests/responses:
WOPI_DEBUG=true
Logs appear in var/log/dev.log.
Validate WOPI Responses Use WOPI Validator to test endpoints:
wopi-validator validate http://your-app/wopi/files/123 --action=getfileinfo
Common Errors
403 Forbidden: Missing access_token or invalid permissions.
Debug: Check getUserCanWrite() logic.500 Internal Server Error: Unhandled exceptions in your handler.
Debug: Enable APP_DEBUG=true and inspect logs.404 Not Found: Incorrect route configuration.
Debug: Verify routing.yaml and prefix in config/routes.yaml.Custom Actions
Extend the bundle by adding new WOPI actions (e.g., copy, rename):
public function copyFile(WopiRequest $request, string $newFileId): void
{
$this->storage->copy($request->getFileId(), $newFileId);
}
Register the action in routing.yaml:
wopi_copy:
path: /files/{fileId}/copy
methods: [POST]
defaults: { _controller: 'App\Controller\WopiController::copy' }
Middleware Integration Add Symfony middleware to pre-process
How can I help you explore Laravel packages today?