feat: improve bbcode builtins

This commit is contained in:
2025-11-10 23:51:44 +02:00
parent 72dbf9b349
commit 9b8bb08a03
9 changed files with 240 additions and 13 deletions

View File

@@ -29,7 +29,7 @@ class BBCodeController extends OCSController {
}
/**
* Get all BBCodes
* Get all BBCodes (excludes builtin codes)
*
* @return DataResponse<Http::STATUS_OK, list<array<string, mixed>>, array{}>
*
@@ -40,7 +40,7 @@ class BBCodeController extends OCSController {
#[ApiRoute(verb: 'GET', url: '/api/bbcodes')]
public function index(): DataResponse {
try {
$bbcodes = $this->bbCodeMapper->findAll();
$bbcodes = $this->bbCodeMapper->findAllNonBuiltin();
return new DataResponse(array_map(fn ($b) => $b->jsonSerialize(), $bbcodes));
} catch (\Exception $e) {
$this->logger->error('Error fetching BBCodes: ' . $e->getMessage());
@@ -67,6 +67,25 @@ class BBCodeController extends OCSController {
}
}
/**
* Get builtin BBCodes (for help dialog)
*
* @return DataResponse<Http::STATUS_OK, list<array<string, mixed>>, array{}>
*
* 200: Builtin BBCodes returned
*/
#[NoAdminRequired]
#[ApiRoute(verb: 'GET', url: '/api/bbcodes/builtin')]
public function builtin(): DataResponse {
try {
$bbcodes = $this->bbCodeMapper->findAllBuiltin();
return new DataResponse(array_map(fn ($b) => $b->jsonSerialize(), $bbcodes));
} catch (\Exception $e) {
$this->logger->error('Error fetching builtin BBCodes: ' . $e->getMessage());
return new DataResponse(['error' => 'Failed to fetch BBCodes'], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* Get a single BBCode
*
@@ -76,10 +95,17 @@ class BBCodeController extends OCSController {
* 200: BBCode returned
*/
#[NoAdminRequired]
#[RequirePermission('canAccessAdminTools')]
#[ApiRoute(verb: 'GET', url: '/api/bbcodes/{id}')]
public function show(int $id): DataResponse {
try {
$bbcode = $this->bbCodeMapper->find($id);
// Prevent access to builtin BBCodes
if ($bbcode->getIsBuiltin()) {
return new DataResponse(['error' => 'Cannot access builtin BBCode'], Http::STATUS_FORBIDDEN);
}
return new DataResponse($bbcode->jsonSerialize());
} catch (DoesNotExistException $e) {
return new DataResponse(['error' => 'BBCode not found'], Http::STATUS_NOT_FOUND);
@@ -112,6 +138,7 @@ class BBCodeController extends OCSController {
$bbcode->setDescription($description);
$bbcode->setEnabled($enabled);
$bbcode->setParseInner($parseInner);
$bbcode->setIsBuiltin(false); // User-created BBCodes are never builtin
$bbcode->setCreatedAt(time());
/** @var \OCA\Forum\Db\BBCode */
@@ -143,6 +170,11 @@ class BBCodeController extends OCSController {
try {
$bbcode = $this->bbCodeMapper->find($id);
// Prevent updating builtin BBCodes
if ($bbcode->getIsBuiltin()) {
return new DataResponse(['error' => 'Cannot update builtin BBCode'], Http::STATUS_FORBIDDEN);
}
if ($tag !== null) {
$bbcode->setTag($tag);
}
@@ -184,6 +216,12 @@ class BBCodeController extends OCSController {
public function destroy(int $id): DataResponse {
try {
$bbcode = $this->bbCodeMapper->find($id);
// Prevent deleting builtin BBCodes
if ($bbcode->getIsBuiltin()) {
return new DataResponse(['error' => 'Cannot delete builtin BBCode'], Http::STATUS_FORBIDDEN);
}
$this->bbCodeMapper->delete($bbcode);
return new DataResponse(['success' => true]);
} catch (DoesNotExistException $e) {

View File

@@ -24,6 +24,8 @@ use OCP\AppFramework\Db\Entity;
* @method void setEnabled(bool $value)
* @method bool getParseInner()
* @method void setParseInner(bool $value)
* @method bool getIsBuiltin()
* @method void setIsBuiltin(bool $value)
* @method int getCreatedAt()
* @method void setCreatedAt(int $value)
*/
@@ -33,6 +35,7 @@ class BBCode extends Entity implements JsonSerializable {
protected $description;
protected $enabled;
protected $parseInner;
protected $isBuiltin;
protected $createdAt;
public function __construct() {
@@ -42,6 +45,7 @@ class BBCode extends Entity implements JsonSerializable {
$this->addType('description', 'string');
$this->addType('enabled', 'boolean');
$this->addType('parseInner', 'boolean');
$this->addType('isBuiltin', 'boolean');
$this->addType('createdAt', 'integer');
}
@@ -53,6 +57,7 @@ class BBCode extends Entity implements JsonSerializable {
'description' => $this->getDescription(),
'enabled' => $this->getEnabled(),
'parseInner' => $this->getParseInner(),
'isBuiltin' => $this->getIsBuiltin(),
'createdAt' => $this->getCreatedAt(),
];
}

View File

@@ -79,4 +79,38 @@ class BBCodeMapper extends QBMapper {
$qb->select('*')->from($this->getTableName());
return $this->findEntities($qb);
}
/**
* Find all non-builtin BBCodes (excludes builtin codes from admin management)
*
* @return array<BBCode>
*/
public function findAllNonBuiltin(): array {
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()
->eq('is_builtin', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))
);
return $this->findEntities($qb);
}
/**
* Find all builtin BBCodes (for help dialog)
*
* @return array<BBCode>
*/
public function findAllBuiltin(): array {
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()
->eq('is_builtin', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))
);
return $this->findEntities($qb);
}
}

View File

@@ -20,7 +20,7 @@ class UserStatsMapper extends QBMapper {
public function __construct(
IDBConnection $db,
) {
parent::__construct($db, Application::tableName('user_stats'), UserStats::class);
parent::__construct($db, Application::tableName('forum_user_stats'), UserStats::class);
}
/**

View File

@@ -86,11 +86,11 @@ class Version1Date20251106004226 extends SimpleMigrationStep {
}
private function createUserStatsTable(ISchemaWrapper $schema): void {
if ($schema->hasTable('user_stats')) {
if ($schema->hasTable('forum_user_stats')) {
return;
}
$table = $schema->createTable('user_stats');
$table = $schema->createTable('forum_user_stats');
$table->addColumn('user_id', 'string', [
'notnull' => true,
'length' => 64,
@@ -311,6 +311,10 @@ class Version1Date20251106004226 extends SimpleMigrationStep {
'notnull' => true,
'default' => true,
]);
$table->addColumn('is_builtin', 'boolean', [
'notnull' => true,
'default' => false,
]);
$table->addColumn('created_at', 'integer', [
'notnull' => true,
'unsigned' => true,
@@ -560,6 +564,20 @@ class Version1Date20251106004226 extends SimpleMigrationStep {
$userManager = \OC::$server->get(\OCP\IUserManager::class);
$timestamp = time();
// Check if data has already been seeded by looking for the Admin role
$qb = $db->getQueryBuilder();
$qb->select('id')
->from('forum_roles')
->where($qb->expr()->eq('name', $qb->createNamedParameter('Admin')));
$result = $qb->executeQuery();
$exists = $result->fetch();
$result->closeCursor();
// If data already exists, skip seeding to avoid duplicate key errors
if ($exists) {
return;
}
// Create default roles
$qb = $db->getQueryBuilder();
@@ -693,10 +711,11 @@ class Version1Date20251106004226 extends SimpleMigrationStep {
// Create default BBCodes
// Note: Most BBCode tags (b, i, u, s, code, email, url, img, quote, youtube, font, size, color, etc.)
// are now provided by the chriskonnertz/bbcode library and don't need to be stored in the database.
// are provided by the chriskonnertz/bbcode library and don't need to be stored in the database.
// We only store custom BBCodes that extend the library's functionality.
$bbcodes = [
['tag' => 'icode', 'replacement' => '<code>{content}</code>', 'description' => 'Inline code', 'parse_inner' => false],
['tag' => 'icode', 'replacement' => '<code>{content}</code>', 'description' => 'Inline code', 'parse_inner' => false, 'is_builtin' => true],
['tag' => 'spoiler', 'replacement' => '<details><summary>{title}</summary>{content}</details>', 'description' => 'Spoilers', 'parse_inner' => false, 'is_builtin' => true],
];
foreach ($bbcodes as $bbcode) {
@@ -708,6 +727,7 @@ class Version1Date20251106004226 extends SimpleMigrationStep {
'description' => $qb->createNamedParameter($bbcode['description']),
'enabled' => $qb->createNamedParameter(true, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
'parse_inner' => $qb->createNamedParameter($bbcode['parse_inner'], \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
'is_builtin' => $qb->createNamedParameter($bbcode['is_builtin'], \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
'created_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
])
->executeStatement();
@@ -806,7 +826,7 @@ class Version1Date20251106004226 extends SimpleMigrationStep {
// Create user stats for admin (who created the welcome post/thread)
$qb = $db->getQueryBuilder();
$qb->insert('user_stats')
$qb->insert('forum_user_stats')
->values([
'user_id' => $qb->createNamedParameter('admin'),
'post_count' => $qb->createNamedParameter(1, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),

View File

@@ -886,7 +886,7 @@
"/ocs/v2.php/apps/forum/api/bbcodes": {
"get": {
"operationId": "bb_code-index",
"summary": "Get all BBCodes",
"summary": "Get all BBCodes (excludes builtin codes)",
"tags": [
"bb_code"
],
@@ -1203,6 +1203,101 @@
}
}
},
"/ocs/v2.php/apps/forum/api/bbcodes/builtin": {
"get": {
"operationId": "bb_code-builtin",
"summary": "Get builtin BBCodes (for help dialog)",
"tags": [
"bb_code"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Builtin BBCodes returned",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {
"type": "object"
}
}
}
}
}
}
}
}
}
},
"401": {
"description": "Current user is not logged in",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/forum/api/bbcodes/{id}": {
"get": {
"operationId": "bb_code-show",

View File

@@ -7,6 +7,7 @@
<p class="section-description">{{ strings.builtInDescription }}</p>
<div class="bbcode-list">
<!-- Library-provided BBCodes -->
<div v-for="code in builtInCodes" :key="code.tag" class="bbcode-item">
<div class="bbcode-header">
<code class="bbcode-tag">[{{ code.tag }}]</code>
@@ -17,6 +18,18 @@
<code class="example-code">{{ code.example }}</code>
</div>
</div>
<!-- Database builtin BBCodes -->
<div v-for="code in builtinDbCodes" :key="code.id" class="bbcode-item">
<div class="bbcode-header">
<code class="bbcode-tag">[{{ code.tag }}]</code>
<span v-if="code.description" class="bbcode-name">{{ code.description }}</span>
</div>
<div class="bbcode-replacement">
<span class="replacement-label">{{ strings.replacement }}:</span>
<code class="replacement-code">{{ code.replacement }}</code>
</div>
</div>
</div>
</section>
@@ -95,6 +108,7 @@ export default defineComponent({
loading: false,
error: null as string | null,
customCodes: [] as BBCode[],
builtinDbCodes: [] as BBCode[],
builtInCodes: [
{ tag: 'b', name: t('forum', 'Font style bold'), example: '[b]Hello world[/b]' },
@@ -145,7 +159,6 @@ export default defineComponent({
name: t('forum', 'Text-align: right'),
example: '[right]Hello world[/right]',
},
{ tag: 'spoiler', name: t('forum', 'Spoiler'), example: '[spoiler]Hello world[/spoiler]' },
{
tag: 'list',
name: t('forum', 'List'),
@@ -183,13 +196,30 @@ export default defineComponent({
open: {
immediate: true,
handler(newValue) {
if (newValue && this.showCustom && this.customCodes.length === 0) {
this.fetchCustomCodes()
if (newValue) {
// Fetch builtin codes from database
if (this.builtinDbCodes.length === 0) {
this.fetchBuiltinCodes()
}
// Fetch custom codes if enabled
if (this.showCustom && this.customCodes.length === 0) {
this.fetchCustomCodes()
}
}
},
},
},
methods: {
async fetchBuiltinCodes() {
try {
const response = await ocs.get<BBCode[]>('/bbcodes/builtin')
this.builtinDbCodes = response.data || []
} catch (e) {
console.error('Failed to fetch builtin BBCodes:', e)
// Silently fail for builtin codes - not critical
}
},
async fetchCustomCodes() {
if (!this.showCustom) {
return

View File

@@ -186,7 +186,11 @@ export default defineComponent({
tag: 'spoiler',
label: 'Spoiler',
icon: EyeOffIcon,
template: '[spoiler]{text}[/spoiler]',
template: '[spoiler="{value}"]{text}[/spoiler]',
hasValue: true,
placeholder: 'Spoiler title',
promptForContent: true,
contentPlaceholder: 'Spoiler content',
},
] as BBCodeButton[],
}

View File

@@ -90,6 +90,7 @@ export interface BBCode {
description: string | null
enabled: boolean
parseInner: boolean
isBuiltin: boolean
createdAt: number
}