mirror of
https://github.com/chenasraf/nextcloud-forum.git
synced 2026-05-17 17:28:02 +00:00
feat: improve bbcode builtins
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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),
|
||||
|
||||
97
openapi.json
97
openapi.json
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[],
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface BBCode {
|
||||
description: string | null
|
||||
enabled: boolean
|
||||
parseInner: boolean
|
||||
isBuiltin: boolean
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user