diff --git a/lib/Controller/BBCodeController.php b/lib/Controller/BBCodeController.php index 855c162..98ba41e 100644 --- a/lib/Controller/BBCodeController.php +++ b/lib/Controller/BBCodeController.php @@ -29,7 +29,7 @@ class BBCodeController extends OCSController { } /** - * Get all BBCodes + * Get all BBCodes (excludes builtin codes) * * @return DataResponse>, 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>, 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) { diff --git a/lib/Db/BBCode.php b/lib/Db/BBCode.php index b379208..942a7a9 100644 --- a/lib/Db/BBCode.php +++ b/lib/Db/BBCode.php @@ -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(), ]; } diff --git a/lib/Db/BBCodeMapper.php b/lib/Db/BBCodeMapper.php index e400c40..ab85a01 100644 --- a/lib/Db/BBCodeMapper.php +++ b/lib/Db/BBCodeMapper.php @@ -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 + */ + 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 + */ + 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); + } } diff --git a/lib/Db/UserStatsMapper.php b/lib/Db/UserStatsMapper.php index ade755a..01544c5 100644 --- a/lib/Db/UserStatsMapper.php +++ b/lib/Db/UserStatsMapper.php @@ -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); } /** diff --git a/lib/Migration/Version1Date20251106004226.php b/lib/Migration/Version1Date20251106004226.php index 85b8619..e20e197 100644 --- a/lib/Migration/Version1Date20251106004226.php +++ b/lib/Migration/Version1Date20251106004226.php @@ -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' => '{content}', 'description' => 'Inline code', 'parse_inner' => false], + ['tag' => 'icode', 'replacement' => '{content}', 'description' => 'Inline code', 'parse_inner' => false, 'is_builtin' => true], + ['tag' => 'spoiler', 'replacement' => '
{title}{content}
', '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), diff --git a/openapi.json b/openapi.json index df6cedd..a1a49f7 100644 --- a/openapi.json +++ b/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", diff --git a/src/components/BBCodeHelpDialog.vue b/src/components/BBCodeHelpDialog.vue index 81b659f..f0e8f72 100644 --- a/src/components/BBCodeHelpDialog.vue +++ b/src/components/BBCodeHelpDialog.vue @@ -7,6 +7,7 @@

{{ strings.builtInDescription }}

+
[{{ code.tag }}] @@ -17,6 +18,18 @@ {{ code.example }}
+ + +
+
+ [{{ code.tag }}] + {{ code.description }} +
+
+ {{ strings.replacement }}: + {{ code.replacement }} +
+
@@ -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('/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 diff --git a/src/components/BBCodeToolbar.vue b/src/components/BBCodeToolbar.vue index 1aa611b..b639cf3 100644 --- a/src/components/BBCodeToolbar.vue +++ b/src/components/BBCodeToolbar.vue @@ -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[], } diff --git a/src/types/models.ts b/src/types/models.ts index 6ac8d11..e76fc6b 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -90,6 +90,7 @@ export interface BBCode { description: string | null enabled: boolean parseInner: boolean + isBuiltin: boolean createdAt: number }