Feature request: reference providers and Smart Picker entries for ExApps
I maintainer di solito rispondono entro 3 giorni
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Idoneità per principianti
- 35/100
Direzione di ricerca
Start with appinfo/routes.php near the top-menu routes, the ex_ui_top_menu model, ScriptsService.php, and nc_py_api/ex_app/ui/top_menu.py. Compare the server reference-provider interfaces and ReferenceManager behavior with the proposed resolve and picker paths. Done should cover registration, discovery, resolution, browser picker integration, and the documented security and CSP constraints.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Feature request: reference providers and Smart Picker entries for ExApps
Hello, and thank you for AppAPI.
I want to write an ExApp that adds an entry to the Smart Picker and changes its own links into rich widgets. I looked for an API for this and I did not find one. If I missed something that is already possible today, please tell me, and I am sorry for the noise.
If the API is really absent, I would like to ask if you want this feature in AppAPI. I did some tests to see if it is possible, and I write the results below. I hope they help. I am happy to do the work if you agree, but the design is yours — my sketch is only a start for the discussion.
What an ExApp cannot do today
A PHP app can register a reference provider. I do not see a way for an ExApp to do this. So an ExApp cannot do these things:
- Show its own entry in the Smart Picker menu.
- Change a link into a rich widget in Text, Talk or Deck.
- Give its own frontend code to the picker dialog.
AppAPI has file actions, top menu entries, scripts, styles and declarative settings. This looks like one of the few UI integrations that is not there yet.
The server API, for context
Four files hold most of it:
IReferenceProvider—matchReference(),resolveReference(),getCachePrefix(),getCacheKey().IDiscoverableReferenceProvider— addsgetId(),getTitle(),getOrder(),getIconUrl().ISearchableReferenceProvider— addsgetSupportedSearchProviderIds().ReferenceManager— finds the providers and keeps the results in a cache for one hour.
The profile app is a short example: it registers the provider and a RenderReferenceEvent listener in Application.php#L31-L32, the listener adds one script in ProfilePickerReferenceListener.php#L46, and the script calls registerCustomPickerElement() in reference.js#L24-L41.
What I tested
I did not want to guess, so I made a small echo ExApp on a local instance with HaRP. The question was: can the picker code in the browser speak to the ExApp directly? The answer changes the design, so I give the data here. Please correct me if a test is wrong or if my setup is not a normal one.
1. The access level of the route is correct. The ExApp had three routes: ^/public/.* (PUBLIC), ^/user/.* (USER), ^/admin/.* (ADMIN).
| Path | No user | admin |
bob |
|---|---|---|---|
/public/x |
200 | 200 | 200 |
/user/x |
403 | 200 | 200 |
/admin/x |
403 | 200 | 403 |
2. A fetch() from a Nextcloud page reaches the ExApp. The browser sent this from /index.php/apps/dashboard/ as the user admin:
fetch("/exapps/echo_app/user/resolve", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Nc-Page-Url": location.href },
body: JSON.stringify({ pageUrl: location.href }),
})
The ExApp received this:
{
"method": "POST",
"path": "/user/resolve",
"headers": {
"x-nc-page-url": "http://nextcloud.local/index.php/apps/dashboard/",
"origin": "http://nextcloud.local",
"cookie": "oc_sessionPassphrase=...",
"ex-app-id": "echo_app",
"ex-app-version": "1.0.0",
"authorization-app-api": "YWRtaW46MDEyMzQ1Njc4OWFiY2RlZg==",
"aa-version": "32"
},
"body": "{\"pageUrl\":\"http://nextcloud.local/index.php/apps/dashboard/\"}"
}
The value of authorization-app-api is admin:0123456789abcdef. The ExApp thus knows the user, and the secret shows that HaRP sent the request.
3. The browser can send the page URL. All three ways work: the query string, a custom header, and the JSON body. HaRP keeps the method, the query string, the body and all client headers. HaRP removes only the /exapps/<appid> prefix from the path (haproxy_agent.py#L481) and adds four headers (haproxy.cfg.template#L78-L81).
4. The ExApp can give JavaScript to the page. The CSP of the server uses script-src-elem 'strict-dynamic'. A script that a trusted script adds is therefore also trusted. A <script src="/exapps/echo_app/public/w.js"> ran and changed the DOM. No nonce was necessary.
5. There is no CSRF check on this path. The request had no requesttoken header, but the status was 200. The path does not go through the PHP router of the server, so the CSRF code does not run. Please see the open questions at the end — I think this one needs your opinion most.
For context, this is how HaRP does the authentication: it reads the oc_sessionPassphrase cookie (spoe-agent.conf#L16), sends all headers to HarpController::getUserInfo(), and gets {user_id, access_level} (haproxy_agent.py#L544-L585).
Can the ExApp give its own Vue component?
This was the part I was least sure about, so I made a second ExApp that serves a Vue component over /exapps/<id>/public/picker.js. A page in Talk loaded that file, and the component then ran in the page. These things worked:
mounted: true
itemCount: 3
firstItem: "budget #1 (from page http://nextcloud.local/index.php/apps/spreed/)"
pageLine: "page seen by ExApp: http://nextcloud.local/index.php/apps/spreed/"
submitted: "https://vueproto.test/vueproto_picker/1"
renderResultOk: true
The component mounted, the search field called the ExApp for each keystroke, the ExApp saw the URL of the page, and a click on a result sent the submit event that the picker dialog reads. A widget for the resolved link also rendered. The browser reported no errors.
I found three conditions. They are not problems, but an ExApp author must know them, so I think the documentation should say them.
1. window.Vue does not exist. Nextcloud gives each app its own copy of Vue, so there is no shared one on the page. The ExApp must thus add Vue to its own file. The runtime-only build is about 108 KB before compression. I kept it inside a closure, and window.Vue was still undefined after the test, so it does not disturb the copy of Nextcloud.
2. The CSP does not permit 'unsafe-eval'. A Vue component with a string template therefore fails with an EvalError, because the template compiler of Vue uses eval. Render functions (h(...)) work correctly. A normal ExApp with a build step has no problem here, because the build step compiles the .vue files. I saw the error only because I wrote the test file by hand.
3. NcCustomPickerRenderResult is not a global object. The picker dialog reads only .element and .object of the result, and it makes no instanceof test (please see dist/core-common.js). A simple object { element, object } is thus sufficient. But it would be nicer if AppAPI gave a small helper to the ExApps, so that they do not depend on this detail.
For completeness: the functions window._registerCustomPickerElement and window._registerWidget are real globals, and dist/core-common.js sets them. An ExApp script can thus register itself without an import. My first tests showed undefined for them, but that was an error in my test tool, not in Nextcloud.
One possible approach
This is only a sketch. If you prefer a different structure, I am happy to follow it.
The idea is to divide the work in two parts, because the two parts have different speed needs:
resolve (slow, cached, no browser) ExApp <-- AppAPI PHP <-- ReferenceManager
picker (fast, many requests) ExApp <-- HaRP <-- browser
Resolve path — keep it in PHP. matchReference() and resolveReference() run when no browser is present, for example in Talk, in cron and for the mobile clients. The server also keeps the result in a cache for one hour (ReferenceManager.php#L29), so the speed of this path is not important. AppAPI can send these two calls to the ExApp with the usual signed request.
Picker path — let the browser speak to the ExApp. The ExApp gives its own picker script. The script speaks to /exapps/<id>/… and sends the page URL. AppAPI is not in this path, so a search for each keystroke stays fast.
A rough list of the parts
-
Table
ex_ui_reference_providers. Columns something like:appid,provider_id,title,icon_url,order,search_providers_ids,script_path,match_patterns,picker_size. The shape ofex_ui_top_menulooks like a good model. -
OCS endpoints.
POST,DELETEandGETon/api/v1/ui/reference-provider, near the top menu routes inroutes.php#L124. -
An anonymous provider object for each row. Please see the next section. The object extends
ADiscoverableReferenceProviderand implementsISearchableReferenceProvider.matchReference()can use the stored patterns, so no network call is necessary for a link that does not match. -
A listener for
RenderReferenceEvent. It can add the picker script of each ExApp with theproxy_jsmethod that AppAPI already has (ScriptsService.php#L88-L106). -
The
nc_py_apiside.nc.ui.reference_provider.register(...)and.unregister(...), in the shape ofui/top_menu.py, with a sync class and an async class. -
Possibly a small JS helper. Condition 3 of the Vue section shows that an ExApp must build the render result itself today. A helper from AppAPI, for example
registerExAppPicker(id, mountFn), would hide that detail and would also hide the choice between/exapps/and/proxy/. Please tell me if you want this, or if a documentation page is sufficient.
The ExApp then writes a small script with the standard function. Please note the render function: a string template does not work, because of the CSP.
// Vue comes from the ExApp's own bundle, not from `window`.
import { createApp, h } from 'vue'
window._registerCustomPickerElement('my_picker', (el, { providerId, accessible }) => {
const app = createApp({
render: () => h('div', [ /* ... */ ]), // render function, not a template
})
app.mount(el)
return { element: el, object: app } // the dialog reads only these two keys
}, (el, renderResult) => {
renderResult.object.unmount()
}, 'normal')
The picker dialog reads the result from a submit event on the element, so I do not think a new contract is necessary.
How to register one provider for each ExApp
I first thought this was a problem, because the server takes a class name, not an object: IRegistrationContext::registerReferenceProvider(string $class). The number of ExApps is not known before the system starts.
Then I saw that AppAPI already solves this for the Task Processing providers. It makes a synthetic class name, connects that name to an anonymous object with registerService(), and gives only the name to the server: TaskProcessingService::registerExAppTaskProcessingProviders() and getAnonymousExAppProvider(). The same method looks correct here:
$className = '\\OCA\\AppAPI\\' . $row['appid'] . '\\' . $row['provider_id'];
$provider = $this->getAnonymousReferenceProvider($row); // anonymous class
$context->registerService($className, static fn () => $provider);
$context->registerReferenceProvider($className);
The class does not have to exist as a file. ServerContainer::query() sends each name that starts with OCA\ to the container of that app (ServerContainer.php#L126-L141), where registerService() put the object. registerReferenceProvider() and registerTaskProcessingProvider() have the same signature.
I tested this with a small app that registers three anonymous providers. GET /ocs/v2.php/references/providers showed all three:
{ "id": "alpha_picker", "title": "Alpha Picker", "order": 10, "search_providers_ids": ["alpha_app-search"] },
{ "id": "beta_picker", "title": "Beta Picker", "order": 20, "search_providers_ids": ["beta_app-search"] },
{ "id": "gamma_picker", "title": "Gamma Picker", "order": 30, "search_providers_ids": ["gamma_app-search"] }
GET /ocs/v2.php/references/resolve also sent each link to the correct object: https://x.test/beta_picker/42 gave "Resolved by beta_picker" with the rich object type beta_picker_widget.
So there is no maximum number of providers, and the server needs no change. This was your own pattern — I only checked that it also works for reference providers.
One small note, and please tell me if I read this wrong: I do not find a caller for registerExAppTaskProcessingProviders() at this time. The GetTaskProcessingProvidersEvent listener seems to do the work instead (GetTaskProcessingProvidersListener.php). The Reference Provider API has no equivalent event, so the registration would have to happen in Application::register(). If you moved away from that pattern for a reason, I would like to know it before I use it again.
Open questions
These are the points where I do not want to decide alone.
1. The /exapps/ path is not always present. The admin must add the reverse proxy rule, and a standard installation does not have it. The picker script would give a 404 on such a server. One option is for AppAPI to choose between /exapps/<id>/… and the PHP route /proxy/{appId}/{other} when it makes the page, and to give the correct base URL to the script in an initial state. The PHP route is slower, but it is always available. Is this the behaviour you want?
2. HaRP does not get the verb of the route. HarpService::getHarpExApp() sends only url, access_level and bruteforce_protection. The PHP proxy matches the URL and the verb (ExAppProxyController.php#L320), but HaRP matches only the URL. A route with verb="GET" thus also accepts a POST through /exapps/. This is independent of this feature, and possibly it is intentional. I mention it only because this feature would use that path more than before. Do you want a separate issue for it?
3. CSRF on the /exapps/ path. Test 5 above shows that there is no CSRF protection there. Each ExApp handler is therefore the only control. I think the documentation should tell ExApp authors to check the Origin header, and a Content-Type: application/json body also causes a preflight request. But you know the threat model of AppAPI much better than I do, so I would like your opinion before I write anything here.
Offer
If you like this direction, I am happy to write it: the app_api part and the nc_py_api part together, with a small example ExApp that shows the Vue picker. I can also start with only a part of it, or with a draft PR for an early review, if that is easier for you.
The two test ExApps above are throwaway code, but I can clean them into an example app if that helps the review.
If you do not want this feature, or if the timing is wrong, that is completely fine — please just say so and I will not take it further.
Thank you for reading this long issue.
Tested on Nextcloud master (f3337c9), AppAPI main (a19f559), HaRP (e28dcb5), with Vue 3.5.41 and Chromium.
- Lingua principale
- PHP
- Stelle
- 198
- Fork
- 26
- Merge medio
- 2g 14h
- PR unite (30g)
- 46
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di nextcloud/app_api
-
Daemon delete dialog: "Remove all ExApps" checkbox and `removeExApps` parameter have no effectAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
I maintainer di solito rispondono entro 3 giorni
-
deploy discussion enhancement
Difficoltà 5/5 Più di una settimana Idoneità per principianti 30/100
I maintainer di solito rispondono entro 3 giorni
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 65/100
nextcloud/app_api#1021 · 1 commento ·
I maintainer di solito rispondono entro 3 giorni
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 38/100
nextcloud/app_api#1013 · 2 commenti ·
I maintainer di solito rispondono entro 3 giorni
-
daemon enhancement
Difficoltà 3/5 1-2 giorni Idoneità per principianti 52/100
I maintainer di solito rispondono entro 3 giorni
Tutte le issue di nextcloud/app_api
Issue simili
-
sync-en
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
I maintainer di solito rispondono entro 1 giorno
-
sync-en
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 4 giorni
-
Перевод устарел
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 85/100
-
bug
Difficoltà 2/5 Mezza giornata Idoneità per principianti 76/100
m3ue/m3u-editor#1604 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
femiwiki/docker-mediawiki#1497 ·
I maintainer di solito rispondono entro 1 giorno