Skip to content

Commit cbf6e9b

Browse files
committed
feat(api): added new endpoint /api/v3.2/meta
1 parent 9a6ff73 commit cbf6e9b

8 files changed

Lines changed: 430 additions & 5 deletions

File tree

docs/openapi.json

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@
523523
"schema": {
524524
"type": "string",
525525
"default": "id_comment",
526-
"enum": ["id_comment", "id", "usr", "email", "datum"]
526+
"enum": ["id_comment", "id", "usr", "datum"]
527527
}
528528
},
529529
{
@@ -552,7 +552,7 @@
552552
"content": {
553553
"application/json": {
554554
"schema": {},
555-
"example": "{\n \"success\": true,\n \"data\": [\n {\n \"id\": 2,\n \"recordId\": 142,\n \"categoryId\": null,\n \"type\": \"faq\",\n \"username\": \"phpMyFAQ User\",\n \"email\": \"user@example.org\",\n \"comment\": \"Foo! Bar?\",\n \"date\": \"2019-12-24T12:24:57+0100\",\n \"helped\": null\n }\n ],\n \"meta\": {\n \"pagination\": {\n \"total\": 50,\n \"count\": 25,\n \"per_page\": 25,\n \"current_page\": 1,\n \"total_pages\": 2,\n \"links\": {\n \"first\": \"/api/v3.2/comments/142?page=1&per_page=25\",\n \"last\": \"/api/v3.2/comments/142?page=2&per_page=25\",\n \"prev\": null,\n \"next\": \"/api/v3.2/comments/142?page=2&per_page=25\"\n }\n },\n \"sorting\": {\n \"field\": \"id_comment\",\n \"order\": \"asc\"\n }\n }\n }"
555+
"example": "{\n \"success\": true,\n \"data\": [\n {\n \"id\": 2,\n \"recordId\": 142,\n \"categoryId\": null,\n \"type\": \"faq\",\n \"username\": \"phpMyFAQ User\",\n \"comment\": \"Foo! Bar?\",\n \"date\": \"2019-12-24T12:24:57+0100\",\n \"helped\": null\n }\n ],\n \"meta\": {\n \"pagination\": {\n \"total\": 50,\n \"count\": 25,\n \"per_page\": 25,\n \"current_page\": 1,\n \"total_pages\": 2,\n \"links\": {\n \"first\": \"/api/v3.2/comments/142?page=1&per_page=25\",\n \"last\": \"/api/v3.2/comments/142?page=2&per_page=25\",\n \"prev\": null,\n \"next\": \"/api/v3.2/comments/142?page=2&per_page=25\"\n }\n },\n \"sorting\": {\n \"field\": \"id_comment\",\n \"order\": \"asc\"\n }\n }\n }"
556556
}
557557
}
558558
}
@@ -1539,6 +1539,55 @@
15391539
}
15401540
}
15411541
},
1542+
"/api/v3.2/meta": {
1543+
"get": {
1544+
"tags": ["Public Endpoints"],
1545+
"operationId": "getMeta",
1546+
"responses": {
1547+
"200": {
1548+
"description": "Returns bootstrap metadata for the phpMyFAQ instance.",
1549+
"content": {
1550+
"application/json": {
1551+
"schema": {
1552+
"properties": {
1553+
"version": {
1554+
"type": "string",
1555+
"example": "4.0.0"
1556+
},
1557+
"title": {
1558+
"type": "string",
1559+
"example": "phpMyFAQ Codename Porus"
1560+
},
1561+
"language": {
1562+
"type": "string",
1563+
"example": "en"
1564+
},
1565+
"availableLanguages": {
1566+
"type": "object",
1567+
"example": {
1568+
"de": "German",
1569+
"en": "English"
1570+
}
1571+
},
1572+
"enabledFeatures": {
1573+
"type": "object"
1574+
},
1575+
"publicLogoUrl": {
1576+
"type": "string",
1577+
"example": "https://localhost/assets/images/logo-transparent.svg"
1578+
},
1579+
"oauthDiscovery": {
1580+
"type": "object"
1581+
}
1582+
},
1583+
"type": "object"
1584+
}
1585+
}
1586+
}
1587+
}
1588+
}
1589+
}
1590+
},
15421591
"/api/v3.2/news": {
15431592
"get": {
15441593
"tags": ["Public Endpoints"],

docs/openapi.yaml

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,6 @@ paths:
378378
- id_comment
379379
- id
380380
- usr
381-
- email
382381
- datum
383382
- name: order
384383
in: query
@@ -401,7 +400,7 @@ paths:
401400
content:
402401
application/json:
403402
schema: {}
404-
example: "{\n \"success\": true,\n \"data\": [\n {\n \"id\": 2,\n \"recordId\": 142,\n \"categoryId\": null,\n \"type\": \"faq\",\n \"username\": \"phpMyFAQ User\",\n \"email\": \"user@example.org\",\n \"comment\": \"Foo! Bar?\",\n \"date\": \"2019-12-24T12:24:57+0100\",\n \"helped\": null\n }\n ],\n \"meta\": {\n \"pagination\": {\n \"total\": 50,\n \"count\": 25,\n \"per_page\": 25,\n \"current_page\": 1,\n \"total_pages\": 2,\n \"links\": {\n \"first\": \"/api/v3.2/comments/142?page=1&per_page=25\",\n \"last\": \"/api/v3.2/comments/142?page=2&per_page=25\",\n \"prev\": null,\n \"next\": \"/api/v3.2/comments/142?page=2&per_page=25\"\n }\n },\n \"sorting\": {\n \"field\": \"id_comment\",\n \"order\": \"asc\"\n }\n }\n }"
403+
example: "{\n \"success\": true,\n \"data\": [\n {\n \"id\": 2,\n \"recordId\": 142,\n \"categoryId\": null,\n \"type\": \"faq\",\n \"username\": \"phpMyFAQ User\",\n \"comment\": \"Foo! Bar?\",\n \"date\": \"2019-12-24T12:24:57+0100\",\n \"helped\": null\n }\n ],\n \"meta\": {\n \"pagination\": {\n \"total\": 50,\n \"count\": 25,\n \"per_page\": 25,\n \"current_page\": 1,\n \"total_pages\": 2,\n \"links\": {\n \"first\": \"/api/v3.2/comments/142?page=1&per_page=25\",\n \"last\": \"/api/v3.2/comments/142?page=2&per_page=25\",\n \"prev\": null,\n \"next\": \"/api/v3.2/comments/142?page=2&per_page=25\"\n }\n },\n \"sorting\": {\n \"field\": \"id_comment\",\n \"order\": \"asc\"\n }\n }\n }"
405404
'/api/v3.2/faqs/{categoryId}':
406405
get:
407406
tags:
@@ -1087,6 +1086,26 @@ paths:
10871086
application/json:
10881087
schema: {}
10891088
example: '{ "loggedin": false, "error": "Wrong username or password." }'
1089+
/api/v3.2/meta:
1090+
get:
1091+
tags:
1092+
- 'Public Endpoints'
1093+
operationId: getMeta
1094+
responses:
1095+
'200':
1096+
description: 'Returns bootstrap metadata for the phpMyFAQ instance.'
1097+
content:
1098+
application/json:
1099+
schema:
1100+
properties:
1101+
version: { type: string, example: 4.0.0 }
1102+
title: { type: string, example: 'phpMyFAQ Codename Porus' }
1103+
language: { type: string, example: en }
1104+
availableLanguages: { type: object, example: { de: German, en: English } }
1105+
enabledFeatures: { type: object }
1106+
publicLogoUrl: { type: string, example: 'https://localhost/assets/images/logo-transparent.svg' }
1107+
oauthDiscovery: { type: object }
1108+
type: object
10901109
/api/v3.2/news:
10911110
get:
10921111
tags:
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
/**
4+
* Public metadata service for the REST API bootstrap endpoint.
5+
*
6+
* This Source Code Form is subject to the terms of the Mozilla Public License,
7+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
8+
* obtain one at https://mozilla.org/MPL/2.0/.
9+
*
10+
* @package phpMyFAQ
11+
* @author Thorsten Rinne <thorsten@phpmyfaq.de>
12+
* @copyright 2026 phpMyFAQ Team
13+
* @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14+
* @link https://www.phpmyfaq.de
15+
* @since 2026-04-11
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace phpMyFAQ\Api;
21+
22+
use phpMyFAQ\Configuration;
23+
use phpMyFAQ\Helper\LanguageHelper;
24+
25+
final readonly class MetaService
26+
{
27+
public function __construct(
28+
private Configuration $configuration,
29+
) {
30+
}
31+
32+
/**
33+
* @return array{
34+
* version: string,
35+
* title: string,
36+
* language: string,
37+
* availableLanguages: array<string, string>,
38+
* enabledFeatures: array<string, bool>,
39+
* publicLogoUrl: string,
40+
* oauthDiscovery: array<string, bool|string|string[]>
41+
* }
42+
*/
43+
public function getPublicMetadata(): array
44+
{
45+
return [
46+
'version' => $this->configuration->getVersion(),
47+
'title' => $this->configuration->getTitle(),
48+
'language' => $this->configuration->getLanguage()->getLanguage(),
49+
'availableLanguages' => LanguageHelper::getAvailableLanguages(),
50+
'enabledFeatures' => $this->buildEnabledFeatures(),
51+
'publicLogoUrl' => $this->buildPublicLogoUrl(),
52+
'oauthDiscovery' => $this->buildOAuthDiscovery(),
53+
];
54+
}
55+
56+
/**
57+
* @return array<string, bool>
58+
*/
59+
private function buildEnabledFeatures(): array
60+
{
61+
return [
62+
'api' => true,
63+
'oauth2' => $this->toBool($this->configuration->get('oauth2.enable')),
64+
'captcha' => $this->toBool($this->configuration->get('spam.enableCaptchaCode')),
65+
'ldap' => $this->configuration->isLdapActive(),
66+
'elasticsearch' => $this->toBool($this->configuration->get('search.enableElasticsearch')),
67+
'opensearch' => $this->toBool($this->configuration->get('search.enableOpenSearch')),
68+
'sso' => $this->toBool($this->configuration->get('security.ssoSupport')),
69+
'signInWithMicrosoft' => $this->configuration->isSignInWithMicrosoftActive(),
70+
];
71+
}
72+
73+
/**
74+
* @return array<string, bool|string|string[]>
75+
*/
76+
private function buildOAuthDiscovery(): array
77+
{
78+
$apiBaseUrl = rtrim($this->configuration->getDefaultUrl(), characters: '/') . '/api';
79+
80+
return [
81+
'enabled' => $this->toBool($this->configuration->get('oauth2.enable')),
82+
'issuer' => $apiBaseUrl,
83+
'authorizationEndpoint' => $apiBaseUrl . '/oauth/authorize',
84+
'tokenEndpoint' => $apiBaseUrl . '/oauth/token',
85+
'grantTypesSupported' => ['authorization_code', 'client_credentials', 'refresh_token'],
86+
'responseTypesSupported' => ['code'],
87+
'tokenEndpointAuthMethodsSupported' => ['client_secret_basic', 'client_secret_post', 'none'],
88+
];
89+
}
90+
91+
private function buildPublicLogoUrl(): string
92+
{
93+
return rtrim($this->configuration->getDefaultUrl(), characters: '/') . '/assets/images/logo-transparent.svg';
94+
}
95+
96+
private function toBool(mixed $value): bool
97+
{
98+
return $value === true || $value === 1 || $value === '1' || $value === 'true';
99+
}
100+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/**
4+
* The Metadata Controller for the REST API
5+
*
6+
* This Source Code Form is subject to the terms of the Mozilla Public License,
7+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
8+
* obtain one at https://mozilla.org/MPL/2.0/.
9+
*
10+
* @package phpMyFAQ
11+
* @author Thorsten Rinne <thorsten@phpmyfaq.de>
12+
* @copyright 2023-2026 phpMyFAQ Team
13+
* @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14+
* @link https://www.phpmyfaq.de
15+
* @since 2026-04-11
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace phpMyFAQ\Controller\Api;
21+
22+
use OpenApi\Attributes as OA;
23+
use phpMyFAQ\Api\MetaService;
24+
use phpMyFAQ\Controller\AbstractController;
25+
use Symfony\Component\HttpFoundation\JsonResponse;
26+
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
27+
use Symfony\Component\Routing\Attribute\Route;
28+
29+
final class MetaController extends AbstractController
30+
{
31+
private MetaService $metaService;
32+
33+
public function __construct(?MetaService $metaService = null)
34+
{
35+
parent::__construct();
36+
$this->metaService = $metaService ?? new MetaService($this->configuration);
37+
38+
if (!$this->isApiEnabled()) {
39+
throw new UnauthorizedHttpException(challenge: 'API is not enabled');
40+
}
41+
}
42+
43+
#[OA\Get(path: '/api/v3.2/meta', operationId: 'getMeta', tags: ['Public Endpoints'])]
44+
#[OA\Response(
45+
response: 200,
46+
description: 'Returns bootstrap metadata for the phpMyFAQ instance.',
47+
content: new OA\JsonContent(properties: [
48+
new OA\Property(property: 'version', type: 'string', example: '4.0.0'),
49+
new OA\Property(property: 'title', type: 'string', example: 'phpMyFAQ Codename Porus'),
50+
new OA\Property(property: 'language', type: 'string', example: 'en'),
51+
new OA\Property(property: 'availableLanguages', type: 'object', example: [
52+
'de' => 'German',
53+
'en' => 'English',
54+
]),
55+
new OA\Property(property: 'enabledFeatures', type: 'object'),
56+
new OA\Property(
57+
property: 'publicLogoUrl',
58+
type: 'string',
59+
example: 'https://localhost/assets/images/logo-transparent.svg',
60+
),
61+
new OA\Property(property: 'oauthDiscovery', type: 'object'),
62+
]),
63+
)]
64+
#[Route(path: 'v3.2/meta', name: 'api.meta', methods: ['GET'])]
65+
public function index(): JsonResponse
66+
{
67+
return $this->json($this->metaService->getPublicMetadata());
68+
}
69+
}

phpmyfaq/src/services.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use phpMyFAQ\Administration\RecentUsers;
2828
use phpMyFAQ\Administration\RatingStatistics;
2929
use phpMyFAQ\Administration\Session as AdminSession;
30+
use phpMyFAQ\Api\MetaService;
3031
use phpMyFAQ\Attachment\AttachmentCollection;
3132
use phpMyFAQ\Auth as LegacyAuth;
3233
use phpMyFAQ\Auth\ApiKeyAuthenticator;
@@ -93,6 +94,7 @@
9394
use phpMyFAQ\Controller\Administration\TagController as AdminTagController;
9495
use phpMyFAQ\Controller\Administration\UserController as AdminUserController;
9596
use phpMyFAQ\Controller\Api\CategoryController as ApiCategoryController;
97+
use phpMyFAQ\Controller\Api\MetaController as ApiMetaController;
9698
use phpMyFAQ\Controller\Api\OAuth2Controller;
9799
use phpMyFAQ\Controller\Api\CommentController as ApiCommentController;
98100
use phpMyFAQ\Controller\Api\FaqController as ApiFaqController;
@@ -241,6 +243,10 @@
241243
service('phpmyfaq.system'),
242244
]);
243245

246+
$services->set('phpmyfaq.api.meta', MetaService::class)->args([
247+
service('phpmyfaq.configuration'),
248+
]);
249+
244250
$services->set('phpmyfaq.admin.admin-log', AdminLog::class)->args([
245251
service('phpmyfaq.configuration'),
246252
]);
@@ -741,6 +747,9 @@
741747
service('phpmyfaq.glossary'),
742748
service('phpmyfaq.language'),
743749
]);
750+
$services->set(ApiMetaController::class, ApiMetaController::class)->args([
751+
service('phpmyfaq.api.meta'),
752+
]);
744753
$services->set(ApiOpenQuestionController::class, ApiOpenQuestionController::class)->args([
745754
service('phpmyfaq.question'),
746755
]);

0 commit comments

Comments
 (0)