Skip to main content

SuiteCRM integration - Releases

← App details

Nextcloud 35

SuiteCRM integration 3.2.4
Release Details
UpdatedAug. 23, 2026, 8:33 a.m.
Changelog

Deprecation cleanup.

  • Notifier::prepare() now throws \OCP\Notification\UnknownNotificationException instead of \InvalidArgumentException when a notification isn't ours (or is a reminder for an unknown SuiteCRM module). Since Nextcloud 30, \InvalidArgumentException is the deprecated contract and every /ocs/v2.php/apps/notifications poll emitted a level-2 warning naming this notifier; the new class is the NC-30+ replacement. Behaviour is identical (row still suppressed on unknown subject); the warning line stops.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
Signatureg0H49K2pxy0oLASZymk71eZc/SqJV/l4y3hxSShrXRSFql30MsfhwCsKajn1AhPWcbu2y/hnFHf/SToF3IqVyiRLitcd80FgetIzgfyMGrzIyvzvNCngZKf6IHATuMu45deRGUp760yX51KsL9EGFDlbtiZpc1OeazbDpWfpBvqISRXhd0KEo6jhpLZCT9N3kPUOo4DbGzrq6PA3JbJA0zUuYXKoFAMGKvxyhm2TgYP7Q+ET7pkYpV4EVytSbNwBcfV5PPjAG55MdHM2Mg4Y22ppJraR2s7vGec3wl/iX6Oyf2jsjVj4F6In4wI6A+QWllyD4rVzvQ6YOvwTtaHJYckoKcTIs7RpfJiAgjHkzYnVVXnPdXLlBCt7rlCqxSBkqhkQngdobjAAuyYSr1bV+JySSXAH2n6vskEzSC+FNkz5TgzAqMcc8I0VOVpGpDhSpgUb4bbbX8gviMEEWQQu1tUuYFFUVqQcy9Zpt1jF/HcOBEL1k5sg1hHsk6r1DqB75fPqGOugxiNsEMa73iYiaV0HDUrMOybGtLkzDxTxRShJk/zN6D03oUSBAE1gaebu4mjvlz/KWvyh++nJH6AnIhiUbxgPfwTGXFN64qQ1zeKSuSUXrijLjaZ0BE2RQtXUvPjgJlq5V+txa/rNCL5ThHkn0yMiLcqhJB4QEq3P0Ig=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.2.3
Release Details
UpdatedAug. 15, 2026, 4:27 p.m.
Changelog

Admin-panel copy polish.

  • "OAuth authorize endpoint path" label shortened to "Authorize path" so it lines up visually with the other short-form labels ("Instance address", "OAuth client ID / secret", "Redirect URI") in the harmonised grid layout.
  • The trailing helper text ("SuiteCRM 8.10.x default: /Api/authorize. Older installs may use /legacy/oauth2/authorize.") is removed. The field still pre-fills with /Api/authorize on a fresh install via Admin.php's getValueString(..., '/Api/authorize') default, so an admin who never had to touch this row still sees the correct value.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignatureD/6aAuPWTF4VDuLA6GAOcjkW5AQK964/Pp8pVssyhws6pCt4Lpm4KHiN8djxiWA/VRCvLGtN70XmSYkJHBMZi1c6f4J2hawz/QmI+5GV03Y28c3oEt7rVSzNxOk7Q6mugRvyodjc0ar+eoiMPOXLuQoMQhjlUpNyIwILBL5hRVftviknUoP2FwbQeKIecc6biii7QqYtiOy+/s8o1PQRBQSE9/UiHvVEQeKHOWGVWX6ny3bHkzvEASQcWMStsfPR7eaEGZP8zOOeCrchinzdztxxogh0i0NCb/5D/4wao0gn8KNfTo7rtI1/TzjxN/eqgLHm2vd9uXYNk+cT0R51wqESAJrS2xPdYLsBWi6MJUyjXTLTP3ZAhvhYrO3XUqTL6QLDSt0wFGaqXx+xw7OYU3ryl8Ox3hMnhOKs5VuiMc9KSEcGPJ+GY18RyIJFvOLV02rYPdmboeSCiD2/n4beIrdMmfhiyErDmAaO0lo2hoA371UP6ghSorgH/aax4LdSYzJ37Ua8Ssm+nvq2NK9KqZXcPoupmBlAojq3o22oVfQcYr49p57af3pFBd6/MPl2 5sI5is0nhBuVMo6jLghs9ShMWMg2QAYsrTxLNoPiRmfqDNCPl0Z+x+9Rg5dgq2g2YRaEC1cfQEi4p0938FrE+LI02zlBeLokZ8l0/fTysRA=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.1.0
Release Details
UpdatedAug. 14, 2026, 7:50 p.m.
Changelog

Three cross-cutting changes go out in one tag: the Nextcloud 35 compatibility bump, color-brand widget icons across every dashboard surface, and the fixes surfaced by a full OWASP-Top-10 secure-coding review. Nothing changes for the end user's day-to-day interaction with the widgets, but the App Store now offers the update to Nextcloud 35 installs, admins see the SuiteCRM brand mark instead of a mono glyph on the dashboard and settings pages, and the security posture picks up rate limits on the four write endpoints, mime-pinning on the avatar proxy, an admin-config whitelist, plaintext-leak fixes on the OAuth log, and a hardening pass on the 2.x → 3.x config migration so client_secret no longer downgrades to admin-inspectable plaintext on upgrade.

Existing installs on 30–34 continue to work unchanged. Admins on PHP 8.2 need to upgrade PHP before installing this release.

Nextcloud 35 compatibility

  • appinfo/info.xml <nextcloud max-version> bumped from 34 to 35 so the App Store lists this release as compatible with the Nextcloud 35 line. Minimum stays at 30 — the widget code, dashboard API, notification and search integrations all use surface that has been stable across 30–35, so there is no reason to raise the floor.
  • appinfo/info.xml <php min-version> bumped from 8.2 to 8.3 to match Nextcloud 35's own PHP baseline. Nextcloud 35 will not boot on PHP 8.2, so advertising 8.2 support would let admins install the app onto a host that cannot actually run the server hosting it.
  • composer.json PHP constraint lifted to ^8.3 in both require and the config.platform block so composer install resolves against the same platform Nextcloud 35 runs on. The config.platform pin matters for anyone building the release tarball on a workstation that still has PHP 8.2 available — without it, composer would happily select older dependency versions than the ones the server actually loads.
  • appinfo/info.xml <author> order reordered so Kim Haverblad (current maintainer, homepage="https://njordium.com") appears above Julien Veyssier (original 1.x author). The App Store renders <author> elements in document order in the sidebar of apps.nextcloud.com/apps/integration_suitecrm; leading with the maintainer who ships the releases matches what users see in the App Store listing for the release they are actually installing. Julien's contribution is still credited — this is an ordering change, not a removal.

Color widget brand icon

Mirrors the pattern shipped in the sibling integration_forgejo_gitea app, which uses a single brand-color SVG across every dashboard widget header and the admin/personal settings anchor. The img/app-color.svg asset was already in the repository from earlier work but not wired anywhere.

  • css/dashboard.css .icon-suitecrm collapsed from the two-rule mono theme-swap (app-dark.svg on light theme, app.svg on dark theme) to a single rule pointing at img/app-color.svg. The color logo reads well against both themes, so no body.theme--dark override is needed. All nine dashboard widget classes (SuiteCRMWidget, SuiteCRMCalendarWidget, SuiteCRMTasksWidget, SuiteCRMCasesWidget, SuiteCRMPipelineWidget, SuiteCRMActivitiesWidget, SuiteCRMContactsWidget, SuiteCRMAccountsWidget, SuiteCRMLeadsWidget) pick this up through their existing getIconClass(): return 'icon-suitecrm' return, so no per-widget PHP change was needed for the CSS layer.
  • Each widget's getIconUrl() now returns the color asset instead of the mono app.svg. The Nextcloud 30+ dashboard app prefers getIconUrl() over getIconClass() because it renders the icon outside the app's own stylesheet context (mail digests, dashboard permalinks); the two paths now agree.
  • SuiteCRMWidget::iconForModule() and SuiteCRMCalendarWidget::iconForModule() per-item icon fallback (the icon shown next to a row when the module is not Calls/Meetings) is now app-color.svg so a Task or Note row carries the brand mark rather than the mono glyph.
  • Vue settings anchorssrc/components/PersonalSettings.vue and src/components/AdminSettings.vue inline the same collapsed rule so the little icon next to the "Connected accounts" / admin settings heading matches the dashboard widgets.
  • Vue view avatar fallback — the seven views that hand SuiteCRM rows to NcListItem (Activities.vue, Cases.vue, Tasks.vue, Pipeline.vue, RecentContacts.vue, RecentAccounts.vue, RecentLeads.vue) now fall back to app-color.svg instead of app.svg when a row has no per-record avatar, so the visual grammar is consistent across the whole app surface.
  • App-menu icons (img/app.svg, img/app-dark.svg) are unchanged. Nextcloud picks those up by file convention for the top navigation bar, where the mono contract still applies — same split the Forgejo/Gitea app uses.

Secure-coding review fixes (OWASP Top-10 pass)

Ran a full static analysis over lib/, src/, templates/, and appinfo/ against the OWASP Top-10 (2021) taxonomy. Zero High findings. Three Medium and four Low findings are fixed in this release; the JWT-signature note (A02) is documented as accepted-risk in the source and needs no code change.

  • A02 Medium — Migration\CopyLegacyAppConfig copied oc_appconfig rows via a raw QueryBuilder INSERT of (appid, configkey, configvalue), which left the sensitive column at its table default of false. Any 2.x install upgrading to 3.x therefore had its client_secret land under the new app id as admin-inspectable plaintext — visible in occ config:app:get integration_suitecrm client_secret and in Nextcloud support-bundle dumps. Fix routes the write through IAppConfig::setValueString with an explicit sensitive: argument, derived from a new SENSITIVE_APPCONFIG_KEYS class-const whitelist. A structural regression test (CopyLegacyAppConfigTest::testAppConfigCopyPreservesSensitiveFlag) locks the guarantee. The migration only selects configkey + configvalue; the sensitive column was added to oc_appconfig in Nextcloud 31, and selecting it on Nextcloud 30 aborts the entire repair step with SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sensitive'. The whitelist alone gives the same practical protection because client_secret is the only sensitive key this app persists.
  • A04 Medium — write endpoints unbounded. The four #[NoAdminRequired] POST endpoints createFollowupTask, logNote, linkDeckCard, and emailToCase all previously accepted requests one-per-round-trip with no rate ceiling. A compromised NC user session — or a cross-app XSS finding a hole elsewhere in the install — could flood the connected SuiteCRM with thousands of Notes / Tasks / Cases per second, up to the SuiteCRM ACL boundary. Each endpoint now carries #[UserRateThrottle(limit: 30, period: 60)] — 30 writes per minute per user, more than enough for a real human workflow, several orders of magnitude below any abuse curve.
  • A05 Low — avatar Content-Type/cache. getSuiteCRMAvatar() is #[NoCSRFRequired] (so it can be embedded in an <img> src) and cached for 24 hours. The DataDisplayResponse default Content-Type of application/octet-stream combined with a compromised or misconfigured SuiteCRM upstream returning an HTML/SVG blob would let an attacker land a cached blob at the Nextcloud origin. Fix magic-byte-sniffs the payload against a whitelist of JPEG/PNG/GIF/WebP (SVG is deliberately excluded because it can carry <script>) and pins the response Content-Type to the sniffed mime; anything that does not sniff as a real image is served as an empty image/png body. nosniff is already set globally by Nextcloud; this closes the hole for browsers that ignore it.
  • A01 Low — setAdminConfig() accepted arbitrary keys. Mirrored the existing USER_ALLOWED_KEYS pattern with an ADMIN_ALLOWED_KEYS whitelist covering the four keys the admin form actually writes (oauth_instance_url, client_id, client_secret, oauth_authorize_path). Silently drops unknown keys, same permissive-drop model as the user-side endpoint.
  • A09 Low — OAuth error log leaked raw guzzle message. The SuiteCRM OAuth exchange failed log line included 'raw' => $result['error'], which on Guzzle client-error paths can embed the request URL and therefore the one-time authorization code= query parameter. The code is already consumed by the time it hits the log, but log hygiene principle is to redact. Removed the field; the structured http_status / error_code / error_description / error_kind context already carries the actionable diagnostic.
  • Non-OWASP hygiene — URL-encoding consistency. SuiteCRMAPIService.php line 110 built the reminder notification click-through as a raw concat of $suitecrmUrl . '/index.php?module=' . $module . '&action=DetailView&record=' . $elemId. Two sibling call sites (SuiteCRMWidget::buildEventLink() and SuiteCRMCalendarWidget::buildEventLink()) already rawurlencode both parts; the inconsistency was a maintenance trap even though the values are TLS-fetched from the admin-configured SuiteCRM. Third call site now matches.

Widget UX overhaul — 3-dot per-widget settings

Aligns the SuiteCRM widget UX with the sibling integration_forgejo_gitea app: every widget now carries a 3-dot toolbar menu with a Refresh action and a Widget settings modal. Per-user refresh cadence, records-to-show count, and (for eight of the nine widgets) "Only records assigned to me" toggle are stored per widget so users can tune each widget independently.

Settings modal exposes three (four for Calendar / Pipeline) sections per widget: - Refresh frequency — Never / 30s / 1m / 5m / 15m / 30m / 1h (default 5 minutes) - Records to show — 5 / 10 / 15 / 20 / 25 / 50 (default 20). Doubles as the fetch limit sent to the SuiteCRM API so we don't ask for rows we'd drop client-side; server clamps to max(1, min(100, N)) as defence against a tampered client. - Show → Only records assigned to me — checkbox on eight of nine widgets. Defaults ON for Calendar / Cases / Tasks / Pipeline (matches the existing single-user behavior; toggle OFF widens to team-visible records within your SuiteCRM ACL). Defaults OFF for Activities / Contacts / Accounts / Leads (they've always shown any recently-added record; toggle ON filters to your own). Events (reminders) is per-user by SuiteCRM design and has no toggle. - Widget-specific extras — Calendar carries "Include Tasks alongside Meetings and Calls"; Pipeline carries the "Closing this quarter / Top value / Weighted value" framing radio (moved out of PersonalSettings.vue's global block).

  • Widget titles are now SuiteCRM: <Module> (colon-separated), matching the Forgejo widget naming convention. All nine widgets: SuiteCRM: Events, SuiteCRM: Calendar, SuiteCRM: Tasks, SuiteCRM: Cases, SuiteCRM: Pipeline, SuiteCRM: Activities, SuiteCRM: Contacts, SuiteCRM: Accounts, SuiteCRM: Leads.
  • Shared SuiteCRMWidgetShell.vue component replaces the per-widget NcDashboardWidget wrapping. The shell renders the toolbar, the loading / not-connected / error / empty states, the slot for the widget's item list, and the settings modal. The Forgejo sibling inlines this shape per widget; we chose to abstract it here because SuiteCRM has nine widgets that share the same shell — duplicating the toolbar and modal nine times would have been 200+ lines of copy-paste with no functional payoff. The visible UX matches Forgejo exactly: same NcActions layout, same modal structure, same RefreshIntervalPicker behaviour.
  • Ported useAutoRefresh.js composable and RefreshIntervalPicker.vue component verbatim from the Forgejo app (with the app id string swapped) so both apps share the same polling semantics — timer plus wake-signal refetches on tab focus, visibility change, and bfcache restore. setIntervalMs(0) disables the timer, wake-signal refetches still run. Cadence options: never / 30s / 60s / 5min / 15min / 30min / 1h.
  • New per-user pref keys (whitelisted in ConfigController::USER_ALLOWED_KEYS): events_refresh_seconds, calendar_refresh_seconds, tasks_refresh_seconds, cases_refresh_seconds, pipeline_refresh_seconds, activities_refresh_seconds, contacts_refresh_seconds, accounts_refresh_seconds, leads_refresh_seconds. All default to 300s.
  • New GET /widget-config endpoint returns the current value of every widget refresh key plus calendar_show_tasks and pipeline_mode in one round-trip; each widget calls it on mount and passes the seed into useAutoRefresh.setIntervalMs(). Save flow reuses the existing PUT /config endpoint, so the AuthZ gate for widget settings is the same USER_ALLOWED_KEYS whitelist the personal-settings page has always used.
  • Per-widget settings moved out of PersonalSettings.vue and into the widget's own 3-dot menu where they belong:
  • Calendar widget: calendar_show_tasks toggle (include SuiteCRM Tasks alongside Meetings/Calls)
  • Pipeline widget: pipeline_mode radio selector (closing this quarter / top value / weighted) The keys themselves are unchanged, so a 3.0.x install upgrading to 3.1.0 keeps whatever value the user had picked.
  • Shared .scw-* CSS classes (SuiteCRM Widget prefix) added to css/dashboard.css for the item list styling — one place instead of nine scoped <style> blocks with the same declarations. css/dashboard.css was already loaded on every widget page via Util::addStyle() in each widget's load() hook, so no additional PHP registration was needed.
  • Semantic per-widget row icons (via vue-material-design-icons): AccountOutline for Contacts, Handshake for Leads, OfficeBuilding for Accounts, Briefcase for Cases, FormatListChecks for Tasks, TrendingUp for Pipeline. Activities / Calendar / Events dispatch per row type (CalendarClock for Meetings, PhoneOutline for Calls, FormatListChecks for Tasks, Note for Notes, BellRing fallback). Rendered inside a small circular container tinted with --color-primary-element so icons flow with the user's Nextcloud accent. Considered SuiteCRM's own SuitePicons font pack for brand consistency but rejected it (icon font, ~100KB with no CDN, glyph-map not published) — MDI is already installed and gives the same semantic clarity at ~200 bytes per icon.
  • CheckCircleOutline empty-state icon on every widget so a widget with zero results still reads as "you're all caught up" rather than a blank card.
  • List behaviour: capped at max-height: 320px with overflow-y: auto. When the user's picked records count fits, the widget is naturally that height; when it doesn't, the list scrolls internally with a subtle scrollbar and the trailing "Show all →" link stays anchored below.
  • Widget titles renamed to SuiteCRM: <Module> (colon-separated) matching Forgejo's naming convention — SuiteCRM: Cases, SuiteCRM: Pipeline, etc.
  • Dropped IAPIWidget / IAPIWidgetV2 from all nine widget classes so the Vue shell renders instead of NC's server-side WidgetItem card. The Vue shell is what carries the 3-dot toolbar and settings modal; NC's own WidgetItem card doesn't have a slot for those. The getItems* / getItemsV2* methods are kept as inert dead code so re-declaring the interface is a one-line change if a future NC version's dashboard app changes its precedence logic.
  • Built js/ bundles are now committed to git so a git checkout v<tag> on a deploy host lands a runnable app without needing node/npm on the host. Matches integration_forgejo_gitea's convention. Regenerate with npm run build before any release commit.

Notes for maintainers

  • Nextcloud release cadence: whenever a new Nextcloud major ships, bump <nextcloud max-version> in appinfo/info.xml, cross-check the PHP min-version against the new server's system-requirements page, and mirror any PHP floor change into composer.json (require and config.platform). A mismatch between the two is silent until someone tries to build on a workstation with an older PHP.
  • Verify the version-bearing files stay in lock-step before commit: appinfo/info.xml, package.json, package-lock.json, CHANGELOG.md, and the git tag. The npm run verify:version hook covers package.json vs appinfo/info.xml; the rest is manual review at commit time.
  • Icon file convention: img/app.svg + img/app-dark.svg are for the Nextcloud app-menu (picked up by NC by filename, mono contract). img/app-color.svg is the brand mark used inside the app's own widgets and settings pages. Do not repoint img/app.svg to the color asset — the app-menu contract expects mono ink that flows with the theme.
  • OWASP review baseline: 3.1.0 clears 0 High + 0 Medium findings on a post-iteration re-review. Every previously-fixed Medium/Low from the first pass is still applied. Four remaining Low advisories are documented tradeoffs (SuiteCRM ACL delegation on onlyMine=false, sensitive column NC-30 whitelist, committed js/ supply-chain note, reference-provider fan-out on paste). Any future write endpoint added to SuiteCRMAPIController needs its own #[UserRateLimit] attribute or an explicit reason in the class doc why it does not. Any future admin-writable config key needs a line in ADMIN_ALLOWED_KEYS and, if it holds a secret, an entry in the migration's SENSITIVE_APPCONFIG_KEYS. Any future widget-list endpoint needs $limit = $this->clampLimit($limit) before hitting the service.
  • New per-widget pref keys must be added to USER_ALLOWED_KEYS in ConfigController AND to the getWidgetConfig() return object AND to the widget's own Vue loadWidgetConfig() reader. Missing any one of the three silently drops the value from the read path or the save path.
  • Widget shell contract: SuiteCRMWidgetShell.vue emits refresh (user hit the refresh action) and save({ refreshSeconds, onlyMine, maxItems, extras, close }) (user hit Save in the modal). Callers must invoke close() inside their save flow — the shell does not auto-close so the caller can leave the modal open if the save failed and it wants the user to retry without re-typing.
  • Regression call-out: the previous "Follow-up Task from a calendar item" action (README line 55) is not currently reachable in the dashboard UI after the Vue widget rewrite; the modal component TaskFollowupModal.vue and the PHP createFollowupTask endpoint are still live but not wired. Re-adding this as a per-row action in the shell is a 3.2.0 scope item.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignaturepXX3njPlrc5StGaxF2bb6qjBDTWnAuMA/SV2M86Wv2linGxlX0aXL8plrixqrBIq
EQwOIaYFeXQM3k7kiIoYQhYcndlKXFRKoN6v1vo63zao/Zi2xlM6V7UoRGzsmBv7
ACiQo70Ia2ETG8ZF8dVUUlwFZ5Pq5ipccgjQe6EgOVu9fwmJQ+NDdhK4LUo4STRZ
QGsuApeNo7G2QyhsPfj+BkgZ1saicpqT8i0eG7OUBPfiyaB3YvkqV2nl6U3ocDcZ
NgbtkRPQCaIa2JymEzQTchZX/HbEqYrPJgaPd/VvMXTrd59fMz0serAevOZROdNJ
zGz+cx846ow83NdyOK931XIgINi6RWY9f2vtfdJCaxKen4mxtOHDXg0PmII1qJCl
gKDgMu3bOKs3RwtSeqPG3TOSN49PCmB4Wj6nM6QihA/dTpgHOSQ2JmpxKE/Zae/j
8Hmh23juWe4diixVnK4+oxzqfSBS4bqHTMtEsS6L5VDpnBYc75CK2w6vafj4xZ+/
CwlZbhX9T7tVYT9pqrazuBn1FzTm3xeHonKxRGt3OzfIPkM/ehA15ESGvldV57NG
G34UMXHhiijPT6fH0U5cARByHDcwWZSam1Iv+SwfZ8ysKZtQzb2NDrDwEpIfchZu
83qX8vpXhVD3KqFj4PS+MwI/rQ66Fh0hHoD+rBpheNA=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0

Nextcloud 34

SuiteCRM integration 3.2.4
Release Details
UpdatedAug. 23, 2026, 8:33 a.m.
Changelog

Deprecation cleanup.

  • Notifier::prepare() now throws \OCP\Notification\UnknownNotificationException instead of \InvalidArgumentException when a notification isn't ours (or is a reminder for an unknown SuiteCRM module). Since Nextcloud 30, \InvalidArgumentException is the deprecated contract and every /ocs/v2.php/apps/notifications poll emitted a level-2 warning naming this notifier; the new class is the NC-30+ replacement. Behaviour is identical (row still suppressed on unknown subject); the warning line stops.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
Signatureg0H49K2pxy0oLASZymk71eZc/SqJV/l4y3hxSShrXRSFql30MsfhwCsKajn1AhPWcbu2y/hnFHf/SToF3IqVyiRLitcd80FgetIzgfyMGrzIyvzvNCngZKf6IHATuMu45deRGUp760yX51KsL9EGFDlbtiZpc1OeazbDpWfpBvqISRXhd0KEo6jhpLZCT9N3kPUOo4DbGzrq6PA3JbJA0zUuYXKoFAMGKvxyhm2TgYP7Q+ET7pkYpV4EVytSbNwBcfV5PPjAG55MdHM2Mg4Y22ppJraR2s7vGec3wl/iX6Oyf2jsjVj4F6In4wI6A+QWllyD4rVzvQ6YOvwTtaHJYckoKcTIs7RpfJiAgjHkzYnVVXnPdXLlBCt7rlCqxSBkqhkQngdobjAAuyYSr1bV+JySSXAH2n6vskEzSC+FNkz5TgzAqMcc8I0VOVpGpDhSpgUb4bbbX8gviMEEWQQu1tUuYFFUVqQcy9Zpt1jF/HcOBEL1k5sg1hHsk6r1DqB75fPqGOugxiNsEMa73iYiaV0HDUrMOybGtLkzDxTxRShJk/zN6D03oUSBAE1gaebu4mjvlz/KWvyh++nJH6AnIhiUbxgPfwTGXFN64qQ1zeKSuSUXrijLjaZ0BE2RQtXUvPjgJlq5V+txa/rNCL5ThHkn0yMiLcqhJB4QEq3P0Ig=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.2.3
Release Details
UpdatedAug. 15, 2026, 4:27 p.m.
Changelog

Admin-panel copy polish.

  • "OAuth authorize endpoint path" label shortened to "Authorize path" so it lines up visually with the other short-form labels ("Instance address", "OAuth client ID / secret", "Redirect URI") in the harmonised grid layout.
  • The trailing helper text ("SuiteCRM 8.10.x default: /Api/authorize. Older installs may use /legacy/oauth2/authorize.") is removed. The field still pre-fills with /Api/authorize on a fresh install via Admin.php's getValueString(..., '/Api/authorize') default, so an admin who never had to touch this row still sees the correct value.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignatureD/6aAuPWTF4VDuLA6GAOcjkW5AQK964/Pp8pVssyhws6pCt4Lpm4KHiN8djxiWA/VRCvLGtN70XmSYkJHBMZi1c6f4J2hawz/QmI+5GV03Y28c3oEt7rVSzNxOk7Q6mugRvyodjc0ar+eoiMPOXLuQoMQhjlUpNyIwILBL5hRVftviknUoP2FwbQeKIecc6biii7QqYtiOy+/s8o1PQRBQSE9/UiHvVEQeKHOWGVWX6ny3bHkzvEASQcWMStsfPR7eaEGZP8zOOeCrchinzdztxxogh0i0NCb/5D/4wao0gn8KNfTo7rtI1/TzjxN/eqgLHm2vd9uXYNk+cT0R51wqESAJrS2xPdYLsBWi6MJUyjXTLTP3ZAhvhYrO3XUqTL6QLDSt0wFGaqXx+xw7OYU3ryl8Ox3hMnhOKs5VuiMc9KSEcGPJ+GY18RyIJFvOLV02rYPdmboeSCiD2/n4beIrdMmfhiyErDmAaO0lo2hoA371UP6ghSorgH/aax4LdSYzJ37Ua8Ssm+nvq2NK9KqZXcPoupmBlAojq3o22oVfQcYr49p57af3pFBd6/MPl2 5sI5is0nhBuVMo6jLghs9ShMWMg2QAYsrTxLNoPiRmfqDNCPl0Z+x+9Rg5dgq2g2YRaEC1cfQEi4p0938FrE+LI02zlBeLokZ8l0/fTysRA=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.1.0
Release Details
UpdatedAug. 14, 2026, 7:50 p.m.
Changelog

Three cross-cutting changes go out in one tag: the Nextcloud 35 compatibility bump, color-brand widget icons across every dashboard surface, and the fixes surfaced by a full OWASP-Top-10 secure-coding review. Nothing changes for the end user's day-to-day interaction with the widgets, but the App Store now offers the update to Nextcloud 35 installs, admins see the SuiteCRM brand mark instead of a mono glyph on the dashboard and settings pages, and the security posture picks up rate limits on the four write endpoints, mime-pinning on the avatar proxy, an admin-config whitelist, plaintext-leak fixes on the OAuth log, and a hardening pass on the 2.x → 3.x config migration so client_secret no longer downgrades to admin-inspectable plaintext on upgrade.

Existing installs on 30–34 continue to work unchanged. Admins on PHP 8.2 need to upgrade PHP before installing this release.

Nextcloud 35 compatibility

  • appinfo/info.xml <nextcloud max-version> bumped from 34 to 35 so the App Store lists this release as compatible with the Nextcloud 35 line. Minimum stays at 30 — the widget code, dashboard API, notification and search integrations all use surface that has been stable across 30–35, so there is no reason to raise the floor.
  • appinfo/info.xml <php min-version> bumped from 8.2 to 8.3 to match Nextcloud 35's own PHP baseline. Nextcloud 35 will not boot on PHP 8.2, so advertising 8.2 support would let admins install the app onto a host that cannot actually run the server hosting it.
  • composer.json PHP constraint lifted to ^8.3 in both require and the config.platform block so composer install resolves against the same platform Nextcloud 35 runs on. The config.platform pin matters for anyone building the release tarball on a workstation that still has PHP 8.2 available — without it, composer would happily select older dependency versions than the ones the server actually loads.
  • appinfo/info.xml <author> order reordered so Kim Haverblad (current maintainer, homepage="https://njordium.com") appears above Julien Veyssier (original 1.x author). The App Store renders <author> elements in document order in the sidebar of apps.nextcloud.com/apps/integration_suitecrm; leading with the maintainer who ships the releases matches what users see in the App Store listing for the release they are actually installing. Julien's contribution is still credited — this is an ordering change, not a removal.

Color widget brand icon

Mirrors the pattern shipped in the sibling integration_forgejo_gitea app, which uses a single brand-color SVG across every dashboard widget header and the admin/personal settings anchor. The img/app-color.svg asset was already in the repository from earlier work but not wired anywhere.

  • css/dashboard.css .icon-suitecrm collapsed from the two-rule mono theme-swap (app-dark.svg on light theme, app.svg on dark theme) to a single rule pointing at img/app-color.svg. The color logo reads well against both themes, so no body.theme--dark override is needed. All nine dashboard widget classes (SuiteCRMWidget, SuiteCRMCalendarWidget, SuiteCRMTasksWidget, SuiteCRMCasesWidget, SuiteCRMPipelineWidget, SuiteCRMActivitiesWidget, SuiteCRMContactsWidget, SuiteCRMAccountsWidget, SuiteCRMLeadsWidget) pick this up through their existing getIconClass(): return 'icon-suitecrm' return, so no per-widget PHP change was needed for the CSS layer.
  • Each widget's getIconUrl() now returns the color asset instead of the mono app.svg. The Nextcloud 30+ dashboard app prefers getIconUrl() over getIconClass() because it renders the icon outside the app's own stylesheet context (mail digests, dashboard permalinks); the two paths now agree.
  • SuiteCRMWidget::iconForModule() and SuiteCRMCalendarWidget::iconForModule() per-item icon fallback (the icon shown next to a row when the module is not Calls/Meetings) is now app-color.svg so a Task or Note row carries the brand mark rather than the mono glyph.
  • Vue settings anchorssrc/components/PersonalSettings.vue and src/components/AdminSettings.vue inline the same collapsed rule so the little icon next to the "Connected accounts" / admin settings heading matches the dashboard widgets.
  • Vue view avatar fallback — the seven views that hand SuiteCRM rows to NcListItem (Activities.vue, Cases.vue, Tasks.vue, Pipeline.vue, RecentContacts.vue, RecentAccounts.vue, RecentLeads.vue) now fall back to app-color.svg instead of app.svg when a row has no per-record avatar, so the visual grammar is consistent across the whole app surface.
  • App-menu icons (img/app.svg, img/app-dark.svg) are unchanged. Nextcloud picks those up by file convention for the top navigation bar, where the mono contract still applies — same split the Forgejo/Gitea app uses.

Secure-coding review fixes (OWASP Top-10 pass)

Ran a full static analysis over lib/, src/, templates/, and appinfo/ against the OWASP Top-10 (2021) taxonomy. Zero High findings. Three Medium and four Low findings are fixed in this release; the JWT-signature note (A02) is documented as accepted-risk in the source and needs no code change.

  • A02 Medium — Migration\CopyLegacyAppConfig copied oc_appconfig rows via a raw QueryBuilder INSERT of (appid, configkey, configvalue), which left the sensitive column at its table default of false. Any 2.x install upgrading to 3.x therefore had its client_secret land under the new app id as admin-inspectable plaintext — visible in occ config:app:get integration_suitecrm client_secret and in Nextcloud support-bundle dumps. Fix routes the write through IAppConfig::setValueString with an explicit sensitive: argument, derived from a new SENSITIVE_APPCONFIG_KEYS class-const whitelist. A structural regression test (CopyLegacyAppConfigTest::testAppConfigCopyPreservesSensitiveFlag) locks the guarantee. The migration only selects configkey + configvalue; the sensitive column was added to oc_appconfig in Nextcloud 31, and selecting it on Nextcloud 30 aborts the entire repair step with SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sensitive'. The whitelist alone gives the same practical protection because client_secret is the only sensitive key this app persists.
  • A04 Medium — write endpoints unbounded. The four #[NoAdminRequired] POST endpoints createFollowupTask, logNote, linkDeckCard, and emailToCase all previously accepted requests one-per-round-trip with no rate ceiling. A compromised NC user session — or a cross-app XSS finding a hole elsewhere in the install — could flood the connected SuiteCRM with thousands of Notes / Tasks / Cases per second, up to the SuiteCRM ACL boundary. Each endpoint now carries #[UserRateThrottle(limit: 30, period: 60)] — 30 writes per minute per user, more than enough for a real human workflow, several orders of magnitude below any abuse curve.
  • A05 Low — avatar Content-Type/cache. getSuiteCRMAvatar() is #[NoCSRFRequired] (so it can be embedded in an <img> src) and cached for 24 hours. The DataDisplayResponse default Content-Type of application/octet-stream combined with a compromised or misconfigured SuiteCRM upstream returning an HTML/SVG blob would let an attacker land a cached blob at the Nextcloud origin. Fix magic-byte-sniffs the payload against a whitelist of JPEG/PNG/GIF/WebP (SVG is deliberately excluded because it can carry <script>) and pins the response Content-Type to the sniffed mime; anything that does not sniff as a real image is served as an empty image/png body. nosniff is already set globally by Nextcloud; this closes the hole for browsers that ignore it.
  • A01 Low — setAdminConfig() accepted arbitrary keys. Mirrored the existing USER_ALLOWED_KEYS pattern with an ADMIN_ALLOWED_KEYS whitelist covering the four keys the admin form actually writes (oauth_instance_url, client_id, client_secret, oauth_authorize_path). Silently drops unknown keys, same permissive-drop model as the user-side endpoint.
  • A09 Low — OAuth error log leaked raw guzzle message. The SuiteCRM OAuth exchange failed log line included 'raw' => $result['error'], which on Guzzle client-error paths can embed the request URL and therefore the one-time authorization code= query parameter. The code is already consumed by the time it hits the log, but log hygiene principle is to redact. Removed the field; the structured http_status / error_code / error_description / error_kind context already carries the actionable diagnostic.
  • Non-OWASP hygiene — URL-encoding consistency. SuiteCRMAPIService.php line 110 built the reminder notification click-through as a raw concat of $suitecrmUrl . '/index.php?module=' . $module . '&action=DetailView&record=' . $elemId. Two sibling call sites (SuiteCRMWidget::buildEventLink() and SuiteCRMCalendarWidget::buildEventLink()) already rawurlencode both parts; the inconsistency was a maintenance trap even though the values are TLS-fetched from the admin-configured SuiteCRM. Third call site now matches.

Widget UX overhaul — 3-dot per-widget settings

Aligns the SuiteCRM widget UX with the sibling integration_forgejo_gitea app: every widget now carries a 3-dot toolbar menu with a Refresh action and a Widget settings modal. Per-user refresh cadence, records-to-show count, and (for eight of the nine widgets) "Only records assigned to me" toggle are stored per widget so users can tune each widget independently.

Settings modal exposes three (four for Calendar / Pipeline) sections per widget: - Refresh frequency — Never / 30s / 1m / 5m / 15m / 30m / 1h (default 5 minutes) - Records to show — 5 / 10 / 15 / 20 / 25 / 50 (default 20). Doubles as the fetch limit sent to the SuiteCRM API so we don't ask for rows we'd drop client-side; server clamps to max(1, min(100, N)) as defence against a tampered client. - Show → Only records assigned to me — checkbox on eight of nine widgets. Defaults ON for Calendar / Cases / Tasks / Pipeline (matches the existing single-user behavior; toggle OFF widens to team-visible records within your SuiteCRM ACL). Defaults OFF for Activities / Contacts / Accounts / Leads (they've always shown any recently-added record; toggle ON filters to your own). Events (reminders) is per-user by SuiteCRM design and has no toggle. - Widget-specific extras — Calendar carries "Include Tasks alongside Meetings and Calls"; Pipeline carries the "Closing this quarter / Top value / Weighted value" framing radio (moved out of PersonalSettings.vue's global block).

  • Widget titles are now SuiteCRM: <Module> (colon-separated), matching the Forgejo widget naming convention. All nine widgets: SuiteCRM: Events, SuiteCRM: Calendar, SuiteCRM: Tasks, SuiteCRM: Cases, SuiteCRM: Pipeline, SuiteCRM: Activities, SuiteCRM: Contacts, SuiteCRM: Accounts, SuiteCRM: Leads.
  • Shared SuiteCRMWidgetShell.vue component replaces the per-widget NcDashboardWidget wrapping. The shell renders the toolbar, the loading / not-connected / error / empty states, the slot for the widget's item list, and the settings modal. The Forgejo sibling inlines this shape per widget; we chose to abstract it here because SuiteCRM has nine widgets that share the same shell — duplicating the toolbar and modal nine times would have been 200+ lines of copy-paste with no functional payoff. The visible UX matches Forgejo exactly: same NcActions layout, same modal structure, same RefreshIntervalPicker behaviour.
  • Ported useAutoRefresh.js composable and RefreshIntervalPicker.vue component verbatim from the Forgejo app (with the app id string swapped) so both apps share the same polling semantics — timer plus wake-signal refetches on tab focus, visibility change, and bfcache restore. setIntervalMs(0) disables the timer, wake-signal refetches still run. Cadence options: never / 30s / 60s / 5min / 15min / 30min / 1h.
  • New per-user pref keys (whitelisted in ConfigController::USER_ALLOWED_KEYS): events_refresh_seconds, calendar_refresh_seconds, tasks_refresh_seconds, cases_refresh_seconds, pipeline_refresh_seconds, activities_refresh_seconds, contacts_refresh_seconds, accounts_refresh_seconds, leads_refresh_seconds. All default to 300s.
  • New GET /widget-config endpoint returns the current value of every widget refresh key plus calendar_show_tasks and pipeline_mode in one round-trip; each widget calls it on mount and passes the seed into useAutoRefresh.setIntervalMs(). Save flow reuses the existing PUT /config endpoint, so the AuthZ gate for widget settings is the same USER_ALLOWED_KEYS whitelist the personal-settings page has always used.
  • Per-widget settings moved out of PersonalSettings.vue and into the widget's own 3-dot menu where they belong:
  • Calendar widget: calendar_show_tasks toggle (include SuiteCRM Tasks alongside Meetings/Calls)
  • Pipeline widget: pipeline_mode radio selector (closing this quarter / top value / weighted) The keys themselves are unchanged, so a 3.0.x install upgrading to 3.1.0 keeps whatever value the user had picked.
  • Shared .scw-* CSS classes (SuiteCRM Widget prefix) added to css/dashboard.css for the item list styling — one place instead of nine scoped <style> blocks with the same declarations. css/dashboard.css was already loaded on every widget page via Util::addStyle() in each widget's load() hook, so no additional PHP registration was needed.
  • Semantic per-widget row icons (via vue-material-design-icons): AccountOutline for Contacts, Handshake for Leads, OfficeBuilding for Accounts, Briefcase for Cases, FormatListChecks for Tasks, TrendingUp for Pipeline. Activities / Calendar / Events dispatch per row type (CalendarClock for Meetings, PhoneOutline for Calls, FormatListChecks for Tasks, Note for Notes, BellRing fallback). Rendered inside a small circular container tinted with --color-primary-element so icons flow with the user's Nextcloud accent. Considered SuiteCRM's own SuitePicons font pack for brand consistency but rejected it (icon font, ~100KB with no CDN, glyph-map not published) — MDI is already installed and gives the same semantic clarity at ~200 bytes per icon.
  • CheckCircleOutline empty-state icon on every widget so a widget with zero results still reads as "you're all caught up" rather than a blank card.
  • List behaviour: capped at max-height: 320px with overflow-y: auto. When the user's picked records count fits, the widget is naturally that height; when it doesn't, the list scrolls internally with a subtle scrollbar and the trailing "Show all →" link stays anchored below.
  • Widget titles renamed to SuiteCRM: <Module> (colon-separated) matching Forgejo's naming convention — SuiteCRM: Cases, SuiteCRM: Pipeline, etc.
  • Dropped IAPIWidget / IAPIWidgetV2 from all nine widget classes so the Vue shell renders instead of NC's server-side WidgetItem card. The Vue shell is what carries the 3-dot toolbar and settings modal; NC's own WidgetItem card doesn't have a slot for those. The getItems* / getItemsV2* methods are kept as inert dead code so re-declaring the interface is a one-line change if a future NC version's dashboard app changes its precedence logic.
  • Built js/ bundles are now committed to git so a git checkout v<tag> on a deploy host lands a runnable app without needing node/npm on the host. Matches integration_forgejo_gitea's convention. Regenerate with npm run build before any release commit.

Notes for maintainers

  • Nextcloud release cadence: whenever a new Nextcloud major ships, bump <nextcloud max-version> in appinfo/info.xml, cross-check the PHP min-version against the new server's system-requirements page, and mirror any PHP floor change into composer.json (require and config.platform). A mismatch between the two is silent until someone tries to build on a workstation with an older PHP.
  • Verify the version-bearing files stay in lock-step before commit: appinfo/info.xml, package.json, package-lock.json, CHANGELOG.md, and the git tag. The npm run verify:version hook covers package.json vs appinfo/info.xml; the rest is manual review at commit time.
  • Icon file convention: img/app.svg + img/app-dark.svg are for the Nextcloud app-menu (picked up by NC by filename, mono contract). img/app-color.svg is the brand mark used inside the app's own widgets and settings pages. Do not repoint img/app.svg to the color asset — the app-menu contract expects mono ink that flows with the theme.
  • OWASP review baseline: 3.1.0 clears 0 High + 0 Medium findings on a post-iteration re-review. Every previously-fixed Medium/Low from the first pass is still applied. Four remaining Low advisories are documented tradeoffs (SuiteCRM ACL delegation on onlyMine=false, sensitive column NC-30 whitelist, committed js/ supply-chain note, reference-provider fan-out on paste). Any future write endpoint added to SuiteCRMAPIController needs its own #[UserRateLimit] attribute or an explicit reason in the class doc why it does not. Any future admin-writable config key needs a line in ADMIN_ALLOWED_KEYS and, if it holds a secret, an entry in the migration's SENSITIVE_APPCONFIG_KEYS. Any future widget-list endpoint needs $limit = $this->clampLimit($limit) before hitting the service.
  • New per-widget pref keys must be added to USER_ALLOWED_KEYS in ConfigController AND to the getWidgetConfig() return object AND to the widget's own Vue loadWidgetConfig() reader. Missing any one of the three silently drops the value from the read path or the save path.
  • Widget shell contract: SuiteCRMWidgetShell.vue emits refresh (user hit the refresh action) and save({ refreshSeconds, onlyMine, maxItems, extras, close }) (user hit Save in the modal). Callers must invoke close() inside their save flow — the shell does not auto-close so the caller can leave the modal open if the save failed and it wants the user to retry without re-typing.
  • Regression call-out: the previous "Follow-up Task from a calendar item" action (README line 55) is not currently reachable in the dashboard UI after the Vue widget rewrite; the modal component TaskFollowupModal.vue and the PHP createFollowupTask endpoint are still live but not wired. Re-adding this as a per-row action in the shell is a 3.2.0 scope item.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignaturepXX3njPlrc5StGaxF2bb6qjBDTWnAuMA/SV2M86Wv2linGxlX0aXL8plrixqrBIq
EQwOIaYFeXQM3k7kiIoYQhYcndlKXFRKoN6v1vo63zao/Zi2xlM6V7UoRGzsmBv7
ACiQo70Ia2ETG8ZF8dVUUlwFZ5Pq5ipccgjQe6EgOVu9fwmJQ+NDdhK4LUo4STRZ
QGsuApeNo7G2QyhsPfj+BkgZ1saicpqT8i0eG7OUBPfiyaB3YvkqV2nl6U3ocDcZ
NgbtkRPQCaIa2JymEzQTchZX/HbEqYrPJgaPd/VvMXTrd59fMz0serAevOZROdNJ
zGz+cx846ow83NdyOK931XIgINi6RWY9f2vtfdJCaxKen4mxtOHDXg0PmII1qJCl
gKDgMu3bOKs3RwtSeqPG3TOSN49PCmB4Wj6nM6QihA/dTpgHOSQ2JmpxKE/Zae/j
8Hmh23juWe4diixVnK4+oxzqfSBS4bqHTMtEsS6L5VDpnBYc75CK2w6vafj4xZ+/
CwlZbhX9T7tVYT9pqrazuBn1FzTm3xeHonKxRGt3OzfIPkM/ehA15ESGvldV57NG
G34UMXHhiijPT6fH0U5cARByHDcwWZSam1Iv+SwfZ8ysKZtQzb2NDrDwEpIfchZu
83qX8vpXhVD3KqFj4PS+MwI/rQ66Fh0hHoD+rBpheNA=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.0.2
Release Details
UpdatedJuly 29, 2026, 2:54 p.m.
Changelog

App Store metadata refresh, take two. The 3.0.1 upload was rejected by the App Store validator with Element 'summary': [facet 'maxLength'] The value has a length of '166'; this exceeds the allowed maximum length of '128'., so this release trims <summary> under the 128-character cap while keeping the widget-count and coverage cue that made the new listing useful in the first place.

Changed

  • appinfo/info.xml <summary> shortened from 166 to 117 characters to satisfy the App Store's apps/info.xsd maxLength facet on <summary>. New form drops the module enumeration and keeps the "nine widgets" specificity: "Nine dashboard widgets, unified search and notifications for SuiteCRM 8.x. Calendar, tasks, cases, pipeline and more." The full nine-widget breakdown is still in <description>, which the schema does not cap.

Notes for maintainers

The apps/info.xsd caps to remember when editing the top of appinfo/info.xml: - <name> max 40 characters - <summary> max 128 characters - <description> no practical cap (Markdown-friendly, use it for the full pitch)

Add a wc -c check on the summary line to release-nc-app.sh phase 1 to catch this locally next time.

Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignatureX4/pzvjKgONWyCtn4SRPhCb11mYQF8NyrEtydiOD9dttFgiLxLwcYhEQJfdBKLxY
KiG0c53jkSZHI0osL7UoSRWBu61KYv5AoRHS9ajdJSXD7xXR46nl56LH//teun3+
tbLSa2YsyNbIlcXDy8/ZeMrNApHk0HkEi9N8yv/m+sPERK975Oy8CrmgfGk7afuW
AkCY1NuDvZL4I5Dstt30Z6BW94hyiYXtky2W+voCjB2nAd6P/k5gGShDw61BrZnf
q1Y3moshtF/XV5niYLsvsj55mL24E8x22/VeMCYzQMKQtAahO8dcl9YrZ5rqho/b
NrxOFNrRVboUIZu5LSkHJgGC8y4GFwKUeyWtzKK6qUESaGU7qZlgi78ISdrh09yP
RLCmZBdE0QLOK0ZZ+Y9wWrq4/3+eE+5QFGJ3Itc4kvvbw80NIOhFL74NzAH15CuP
AFZ6EHUKNpqZ/9Ek0ppmRAaYrPnBsVqSCvDnRFVL8a3mi6Beul1B2Tixpx0wOKzd
wy5nx/eXEwBXBZJY91CgQDUFNMKMflxlIXgUIpAWUAjEemKlUJojxcmbB3+oNUPz
t8XTqPkcKedrDnLBdmyai6G2CTT187b6gx3u+qX2GT0HY1NG1x9PpH6+0ubL3loP
5GyLf0FkVjBVMgCVx08wRCnYWkTokDobVeDrHDkFiDo=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<35.0.0
Minimum Integer bits32
PHP>=8.2.0
SuiteCRM integration 3.0.0
Release Details
UpdatedJuly 29, 2026, 1:38 p.m.
Changelog

Renames the Nextcloud app id from njordium_suitecrm back to integration_suitecrm. This is the only breaking change in the release. Every setting on your existing 2.x install, admin OAuth config (instance URL, client ID/secret, authorize path), every per-user OAuth token, every widget preference (pipeline_mode, quick_actions_enabled, calendar_show_tasks) carries across automatically via a Repair step that runs on occ upgrade. Users do not need to re-authorise SuiteCRM.

Why the rename. When the fork first shipped in 2.0.0, Julien Veyssier's original integration_suitecrm App Store record was stale and blocked our updates from reaching his ~200 existing installs. The pragmatic response was to rename the fork's app id to njordium_suitecrm so we could ship on the App Store under our own record. In July 2026, Julien transferred ownership of the original record to this fork (see nextcloud/app-certificate-requests#1104 and #1114), which removed the original blocker. 3.0.0 restores the canonical integration_suitecrm app id so:

  • Julien's existing 1.x installs get updates seamlessly from the App Store, no manual reinstall.
  • There's one canonical id on the store, no user confusion about which of two records to install.
  • The GitHub org stays njordium/integration_suitecrm — the App Store app id and the GitHub org name are decoupled.

See docs/upgrade-2.x-to-3.0.md for the end-to-end upgrade walkthrough, rollback procedure, and post-upgrade verification steps.

Changed

  • App id renamed from njordium_suitecrm back to integration_suitecrm. The install folder is now custom_apps/integration_suitecrm/; the old custom_apps/njordium_suitecrm/ will no longer be recognised. Every HTTP route the frontend calls (/apps/integration_suitecrm/*) and every internal string reference has been updated in a single atomic pass across 35 files.
  • occ command name is now occ integration_suitecrm:test-connection. The 2.x form occ njordium_suitecrm:test-connection no longer resolves.
  • OAuth redirect URL on the SuiteCRM OAuth2 client must be updated from <nextcloud>/apps/njordium_suitecrm/oauth-callback to <nextcloud>/apps/integration_suitecrm/oauth-callback. Byte-for-byte match required. Same manual step users hit on the 1.9 to 2.0 upgrade; instructions in docs/upgrade-2.x-to-3.0.md.
  • Webpack bundle filenames flipped from njordium_suitecrm-*.js back to integration_suitecrm-*.js. Direct-install admins with cached JS in a reverse proxy should invalidate the cache on upgrade.
  • Migration\CopyLegacyAppConfig SQL body unchanged, LEGACY_APP_ID constant flipped from integration_suitecrm (2.0.0) to njordium_suitecrm (3.0.0). The class continues to run on every occ upgrade as a post-migration Repair step. On a fresh install (no rows to copy) it's a silent no-op.

Migration path

  • From 2.x (any 2.0-2.6 release) to 3.0.0: occ upgrade runs Migration\CopyLegacyAppConfig which copies every oc_appconfig row where appid='njordium_suitecrm' and every oc_preferences row for the same app id into appid='integration_suitecrm'. Legacy rows are left in place so rollback stays trivial. Encrypted OAuth tokens survive the copy because they live in oc_preferences under app id-scoped rows.
  • From 1.9.x directly to 3.0.0: oc_appconfig and oc_preferences rows are already under integration_suitecrm (that was Julien's app id), so the Repair step is a silent no-op and the deployment picks the settings up under the same id automatically.

Rollback

If 3.0.0 misbehaves, rollback to 2.6.0 is safe because the migration is copy-only, not move: occ app:disable integration_suitecrm && occ app:enable njordium_suitecrm && occ upgrade restores every setting. Detailed steps in docs/upgrade-2.x-to-3.0.md.

Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
Signatureogxzom4kGvfPpSAHfscVLCco55DbReJH1/78dRCmY1oXJ2XiHb1LEXA33NKJMSEt
OBKUO3MKw31GLqvjOSh/q1nIThsgvMviEnvqSHKf3A3Xd9zeJCW0gtuQsFrFGrIV
qsw16PpfY8qwa/WSpV7i3hVEY38BohCGP+GtlTiTg4OZUouyBkt7c5xqO6PlhXBX
fkIJepB1Lf6NLFa9B67Lxuzn0LLXG9BHamrfhCMvENcoJ3gfEXlbqB8/Isxg7i9D
hzgQD+pR/l6nNgJVwT8oatJpeS4SiarOH2RTMxSIkbBU1d9GgXTRSEZ2zLmU1/do
R2W9vcNWnLfNYpIzWVy9UQNG/OLd7aGGMp6Q5EJSKPQGleKLlCzoVwyb7ghNu6fU
NDeEpJQu9wNmCTEWJKzXrDiENJKAAoWHocLv5CL00xK/yGRTXj3of6NOmUfDkAq5
TfHMQcxnpj7PG103qqaZI/JQ3X49MXDbe/BOnXafNyhEXJVReYHTqXfSk+HTE4dm
VwJjN8cjunijsld5PLDWY3xyK8s/or+5xgikAwm5apcI/Masfk/FGsPWq4NJUXQN
VsQOX5KwNrGdPZmxxMdd5Rov8CkC9eE/Yrtwhem8fd1vs8b0T88PPv/224Fwb4jJ
oI9709FIemcZF3xrQTMPnwOmfKIVKiGVlOsva3IbYl8=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<35.0.0
Minimum Integer bits32
PHP>=8.2.0

Nextcloud 33

SuiteCRM integration 3.2.4
Release Details
UpdatedAug. 23, 2026, 8:33 a.m.
Changelog

Deprecation cleanup.

  • Notifier::prepare() now throws \OCP\Notification\UnknownNotificationException instead of \InvalidArgumentException when a notification isn't ours (or is a reminder for an unknown SuiteCRM module). Since Nextcloud 30, \InvalidArgumentException is the deprecated contract and every /ocs/v2.php/apps/notifications poll emitted a level-2 warning naming this notifier; the new class is the NC-30+ replacement. Behaviour is identical (row still suppressed on unknown subject); the warning line stops.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
Signatureg0H49K2pxy0oLASZymk71eZc/SqJV/l4y3hxSShrXRSFql30MsfhwCsKajn1AhPWcbu2y/hnFHf/SToF3IqVyiRLitcd80FgetIzgfyMGrzIyvzvNCngZKf6IHATuMu45deRGUp760yX51KsL9EGFDlbtiZpc1OeazbDpWfpBvqISRXhd0KEo6jhpLZCT9N3kPUOo4DbGzrq6PA3JbJA0zUuYXKoFAMGKvxyhm2TgYP7Q+ET7pkYpV4EVytSbNwBcfV5PPjAG55MdHM2Mg4Y22ppJraR2s7vGec3wl/iX6Oyf2jsjVj4F6In4wI6A+QWllyD4rVzvQ6YOvwTtaHJYckoKcTIs7RpfJiAgjHkzYnVVXnPdXLlBCt7rlCqxSBkqhkQngdobjAAuyYSr1bV+JySSXAH2n6vskEzSC+FNkz5TgzAqMcc8I0VOVpGpDhSpgUb4bbbX8gviMEEWQQu1tUuYFFUVqQcy9Zpt1jF/HcOBEL1k5sg1hHsk6r1DqB75fPqGOugxiNsEMa73iYiaV0HDUrMOybGtLkzDxTxRShJk/zN6D03oUSBAE1gaebu4mjvlz/KWvyh++nJH6AnIhiUbxgPfwTGXFN64qQ1zeKSuSUXrijLjaZ0BE2RQtXUvPjgJlq5V+txa/rNCL5ThHkn0yMiLcqhJB4QEq3P0Ig=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.2.3
Release Details
UpdatedAug. 15, 2026, 4:27 p.m.
Changelog

Admin-panel copy polish.

  • "OAuth authorize endpoint path" label shortened to "Authorize path" so it lines up visually with the other short-form labels ("Instance address", "OAuth client ID / secret", "Redirect URI") in the harmonised grid layout.
  • The trailing helper text ("SuiteCRM 8.10.x default: /Api/authorize. Older installs may use /legacy/oauth2/authorize.") is removed. The field still pre-fills with /Api/authorize on a fresh install via Admin.php's getValueString(..., '/Api/authorize') default, so an admin who never had to touch this row still sees the correct value.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignatureD/6aAuPWTF4VDuLA6GAOcjkW5AQK964/Pp8pVssyhws6pCt4Lpm4KHiN8djxiWA/VRCvLGtN70XmSYkJHBMZi1c6f4J2hawz/QmI+5GV03Y28c3oEt7rVSzNxOk7Q6mugRvyodjc0ar+eoiMPOXLuQoMQhjlUpNyIwILBL5hRVftviknUoP2FwbQeKIecc6biii7QqYtiOy+/s8o1PQRBQSE9/UiHvVEQeKHOWGVWX6ny3bHkzvEASQcWMStsfPR7eaEGZP8zOOeCrchinzdztxxogh0i0NCb/5D/4wao0gn8KNfTo7rtI1/TzjxN/eqgLHm2vd9uXYNk+cT0R51wqESAJrS2xPdYLsBWi6MJUyjXTLTP3ZAhvhYrO3XUqTL6QLDSt0wFGaqXx+xw7OYU3ryl8Ox3hMnhOKs5VuiMc9KSEcGPJ+GY18RyIJFvOLV02rYPdmboeSCiD2/n4beIrdMmfhiyErDmAaO0lo2hoA371UP6ghSorgH/aax4LdSYzJ37Ua8Ssm+nvq2NK9KqZXcPoupmBlAojq3o22oVfQcYr49p57af3pFBd6/MPl2 5sI5is0nhBuVMo6jLghs9ShMWMg2QAYsrTxLNoPiRmfqDNCPl0Z+x+9Rg5dgq2g2YRaEC1cfQEi4p0938FrE+LI02zlBeLokZ8l0/fTysRA=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.1.0
Release Details
UpdatedAug. 14, 2026, 7:50 p.m.
Changelog

Three cross-cutting changes go out in one tag: the Nextcloud 35 compatibility bump, color-brand widget icons across every dashboard surface, and the fixes surfaced by a full OWASP-Top-10 secure-coding review. Nothing changes for the end user's day-to-day interaction with the widgets, but the App Store now offers the update to Nextcloud 35 installs, admins see the SuiteCRM brand mark instead of a mono glyph on the dashboard and settings pages, and the security posture picks up rate limits on the four write endpoints, mime-pinning on the avatar proxy, an admin-config whitelist, plaintext-leak fixes on the OAuth log, and a hardening pass on the 2.x → 3.x config migration so client_secret no longer downgrades to admin-inspectable plaintext on upgrade.

Existing installs on 30–34 continue to work unchanged. Admins on PHP 8.2 need to upgrade PHP before installing this release.

Nextcloud 35 compatibility

  • appinfo/info.xml <nextcloud max-version> bumped from 34 to 35 so the App Store lists this release as compatible with the Nextcloud 35 line. Minimum stays at 30 — the widget code, dashboard API, notification and search integrations all use surface that has been stable across 30–35, so there is no reason to raise the floor.
  • appinfo/info.xml <php min-version> bumped from 8.2 to 8.3 to match Nextcloud 35's own PHP baseline. Nextcloud 35 will not boot on PHP 8.2, so advertising 8.2 support would let admins install the app onto a host that cannot actually run the server hosting it.
  • composer.json PHP constraint lifted to ^8.3 in both require and the config.platform block so composer install resolves against the same platform Nextcloud 35 runs on. The config.platform pin matters for anyone building the release tarball on a workstation that still has PHP 8.2 available — without it, composer would happily select older dependency versions than the ones the server actually loads.
  • appinfo/info.xml <author> order reordered so Kim Haverblad (current maintainer, homepage="https://njordium.com") appears above Julien Veyssier (original 1.x author). The App Store renders <author> elements in document order in the sidebar of apps.nextcloud.com/apps/integration_suitecrm; leading with the maintainer who ships the releases matches what users see in the App Store listing for the release they are actually installing. Julien's contribution is still credited — this is an ordering change, not a removal.

Color widget brand icon

Mirrors the pattern shipped in the sibling integration_forgejo_gitea app, which uses a single brand-color SVG across every dashboard widget header and the admin/personal settings anchor. The img/app-color.svg asset was already in the repository from earlier work but not wired anywhere.

  • css/dashboard.css .icon-suitecrm collapsed from the two-rule mono theme-swap (app-dark.svg on light theme, app.svg on dark theme) to a single rule pointing at img/app-color.svg. The color logo reads well against both themes, so no body.theme--dark override is needed. All nine dashboard widget classes (SuiteCRMWidget, SuiteCRMCalendarWidget, SuiteCRMTasksWidget, SuiteCRMCasesWidget, SuiteCRMPipelineWidget, SuiteCRMActivitiesWidget, SuiteCRMContactsWidget, SuiteCRMAccountsWidget, SuiteCRMLeadsWidget) pick this up through their existing getIconClass(): return 'icon-suitecrm' return, so no per-widget PHP change was needed for the CSS layer.
  • Each widget's getIconUrl() now returns the color asset instead of the mono app.svg. The Nextcloud 30+ dashboard app prefers getIconUrl() over getIconClass() because it renders the icon outside the app's own stylesheet context (mail digests, dashboard permalinks); the two paths now agree.
  • SuiteCRMWidget::iconForModule() and SuiteCRMCalendarWidget::iconForModule() per-item icon fallback (the icon shown next to a row when the module is not Calls/Meetings) is now app-color.svg so a Task or Note row carries the brand mark rather than the mono glyph.
  • Vue settings anchorssrc/components/PersonalSettings.vue and src/components/AdminSettings.vue inline the same collapsed rule so the little icon next to the "Connected accounts" / admin settings heading matches the dashboard widgets.
  • Vue view avatar fallback — the seven views that hand SuiteCRM rows to NcListItem (Activities.vue, Cases.vue, Tasks.vue, Pipeline.vue, RecentContacts.vue, RecentAccounts.vue, RecentLeads.vue) now fall back to app-color.svg instead of app.svg when a row has no per-record avatar, so the visual grammar is consistent across the whole app surface.
  • App-menu icons (img/app.svg, img/app-dark.svg) are unchanged. Nextcloud picks those up by file convention for the top navigation bar, where the mono contract still applies — same split the Forgejo/Gitea app uses.

Secure-coding review fixes (OWASP Top-10 pass)

Ran a full static analysis over lib/, src/, templates/, and appinfo/ against the OWASP Top-10 (2021) taxonomy. Zero High findings. Three Medium and four Low findings are fixed in this release; the JWT-signature note (A02) is documented as accepted-risk in the source and needs no code change.

  • A02 Medium — Migration\CopyLegacyAppConfig copied oc_appconfig rows via a raw QueryBuilder INSERT of (appid, configkey, configvalue), which left the sensitive column at its table default of false. Any 2.x install upgrading to 3.x therefore had its client_secret land under the new app id as admin-inspectable plaintext — visible in occ config:app:get integration_suitecrm client_secret and in Nextcloud support-bundle dumps. Fix routes the write through IAppConfig::setValueString with an explicit sensitive: argument, derived from a new SENSITIVE_APPCONFIG_KEYS class-const whitelist. A structural regression test (CopyLegacyAppConfigTest::testAppConfigCopyPreservesSensitiveFlag) locks the guarantee. The migration only selects configkey + configvalue; the sensitive column was added to oc_appconfig in Nextcloud 31, and selecting it on Nextcloud 30 aborts the entire repair step with SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sensitive'. The whitelist alone gives the same practical protection because client_secret is the only sensitive key this app persists.
  • A04 Medium — write endpoints unbounded. The four #[NoAdminRequired] POST endpoints createFollowupTask, logNote, linkDeckCard, and emailToCase all previously accepted requests one-per-round-trip with no rate ceiling. A compromised NC user session — or a cross-app XSS finding a hole elsewhere in the install — could flood the connected SuiteCRM with thousands of Notes / Tasks / Cases per second, up to the SuiteCRM ACL boundary. Each endpoint now carries #[UserRateThrottle(limit: 30, period: 60)] — 30 writes per minute per user, more than enough for a real human workflow, several orders of magnitude below any abuse curve.
  • A05 Low — avatar Content-Type/cache. getSuiteCRMAvatar() is #[NoCSRFRequired] (so it can be embedded in an <img> src) and cached for 24 hours. The DataDisplayResponse default Content-Type of application/octet-stream combined with a compromised or misconfigured SuiteCRM upstream returning an HTML/SVG blob would let an attacker land a cached blob at the Nextcloud origin. Fix magic-byte-sniffs the payload against a whitelist of JPEG/PNG/GIF/WebP (SVG is deliberately excluded because it can carry <script>) and pins the response Content-Type to the sniffed mime; anything that does not sniff as a real image is served as an empty image/png body. nosniff is already set globally by Nextcloud; this closes the hole for browsers that ignore it.
  • A01 Low — setAdminConfig() accepted arbitrary keys. Mirrored the existing USER_ALLOWED_KEYS pattern with an ADMIN_ALLOWED_KEYS whitelist covering the four keys the admin form actually writes (oauth_instance_url, client_id, client_secret, oauth_authorize_path). Silently drops unknown keys, same permissive-drop model as the user-side endpoint.
  • A09 Low — OAuth error log leaked raw guzzle message. The SuiteCRM OAuth exchange failed log line included 'raw' => $result['error'], which on Guzzle client-error paths can embed the request URL and therefore the one-time authorization code= query parameter. The code is already consumed by the time it hits the log, but log hygiene principle is to redact. Removed the field; the structured http_status / error_code / error_description / error_kind context already carries the actionable diagnostic.
  • Non-OWASP hygiene — URL-encoding consistency. SuiteCRMAPIService.php line 110 built the reminder notification click-through as a raw concat of $suitecrmUrl . '/index.php?module=' . $module . '&action=DetailView&record=' . $elemId. Two sibling call sites (SuiteCRMWidget::buildEventLink() and SuiteCRMCalendarWidget::buildEventLink()) already rawurlencode both parts; the inconsistency was a maintenance trap even though the values are TLS-fetched from the admin-configured SuiteCRM. Third call site now matches.

Widget UX overhaul — 3-dot per-widget settings

Aligns the SuiteCRM widget UX with the sibling integration_forgejo_gitea app: every widget now carries a 3-dot toolbar menu with a Refresh action and a Widget settings modal. Per-user refresh cadence, records-to-show count, and (for eight of the nine widgets) "Only records assigned to me" toggle are stored per widget so users can tune each widget independently.

Settings modal exposes three (four for Calendar / Pipeline) sections per widget: - Refresh frequency — Never / 30s / 1m / 5m / 15m / 30m / 1h (default 5 minutes) - Records to show — 5 / 10 / 15 / 20 / 25 / 50 (default 20). Doubles as the fetch limit sent to the SuiteCRM API so we don't ask for rows we'd drop client-side; server clamps to max(1, min(100, N)) as defence against a tampered client. - Show → Only records assigned to me — checkbox on eight of nine widgets. Defaults ON for Calendar / Cases / Tasks / Pipeline (matches the existing single-user behavior; toggle OFF widens to team-visible records within your SuiteCRM ACL). Defaults OFF for Activities / Contacts / Accounts / Leads (they've always shown any recently-added record; toggle ON filters to your own). Events (reminders) is per-user by SuiteCRM design and has no toggle. - Widget-specific extras — Calendar carries "Include Tasks alongside Meetings and Calls"; Pipeline carries the "Closing this quarter / Top value / Weighted value" framing radio (moved out of PersonalSettings.vue's global block).

  • Widget titles are now SuiteCRM: <Module> (colon-separated), matching the Forgejo widget naming convention. All nine widgets: SuiteCRM: Events, SuiteCRM: Calendar, SuiteCRM: Tasks, SuiteCRM: Cases, SuiteCRM: Pipeline, SuiteCRM: Activities, SuiteCRM: Contacts, SuiteCRM: Accounts, SuiteCRM: Leads.
  • Shared SuiteCRMWidgetShell.vue component replaces the per-widget NcDashboardWidget wrapping. The shell renders the toolbar, the loading / not-connected / error / empty states, the slot for the widget's item list, and the settings modal. The Forgejo sibling inlines this shape per widget; we chose to abstract it here because SuiteCRM has nine widgets that share the same shell — duplicating the toolbar and modal nine times would have been 200+ lines of copy-paste with no functional payoff. The visible UX matches Forgejo exactly: same NcActions layout, same modal structure, same RefreshIntervalPicker behaviour.
  • Ported useAutoRefresh.js composable and RefreshIntervalPicker.vue component verbatim from the Forgejo app (with the app id string swapped) so both apps share the same polling semantics — timer plus wake-signal refetches on tab focus, visibility change, and bfcache restore. setIntervalMs(0) disables the timer, wake-signal refetches still run. Cadence options: never / 30s / 60s / 5min / 15min / 30min / 1h.
  • New per-user pref keys (whitelisted in ConfigController::USER_ALLOWED_KEYS): events_refresh_seconds, calendar_refresh_seconds, tasks_refresh_seconds, cases_refresh_seconds, pipeline_refresh_seconds, activities_refresh_seconds, contacts_refresh_seconds, accounts_refresh_seconds, leads_refresh_seconds. All default to 300s.
  • New GET /widget-config endpoint returns the current value of every widget refresh key plus calendar_show_tasks and pipeline_mode in one round-trip; each widget calls it on mount and passes the seed into useAutoRefresh.setIntervalMs(). Save flow reuses the existing PUT /config endpoint, so the AuthZ gate for widget settings is the same USER_ALLOWED_KEYS whitelist the personal-settings page has always used.
  • Per-widget settings moved out of PersonalSettings.vue and into the widget's own 3-dot menu where they belong:
  • Calendar widget: calendar_show_tasks toggle (include SuiteCRM Tasks alongside Meetings/Calls)
  • Pipeline widget: pipeline_mode radio selector (closing this quarter / top value / weighted) The keys themselves are unchanged, so a 3.0.x install upgrading to 3.1.0 keeps whatever value the user had picked.
  • Shared .scw-* CSS classes (SuiteCRM Widget prefix) added to css/dashboard.css for the item list styling — one place instead of nine scoped <style> blocks with the same declarations. css/dashboard.css was already loaded on every widget page via Util::addStyle() in each widget's load() hook, so no additional PHP registration was needed.
  • Semantic per-widget row icons (via vue-material-design-icons): AccountOutline for Contacts, Handshake for Leads, OfficeBuilding for Accounts, Briefcase for Cases, FormatListChecks for Tasks, TrendingUp for Pipeline. Activities / Calendar / Events dispatch per row type (CalendarClock for Meetings, PhoneOutline for Calls, FormatListChecks for Tasks, Note for Notes, BellRing fallback). Rendered inside a small circular container tinted with --color-primary-element so icons flow with the user's Nextcloud accent. Considered SuiteCRM's own SuitePicons font pack for brand consistency but rejected it (icon font, ~100KB with no CDN, glyph-map not published) — MDI is already installed and gives the same semantic clarity at ~200 bytes per icon.
  • CheckCircleOutline empty-state icon on every widget so a widget with zero results still reads as "you're all caught up" rather than a blank card.
  • List behaviour: capped at max-height: 320px with overflow-y: auto. When the user's picked records count fits, the widget is naturally that height; when it doesn't, the list scrolls internally with a subtle scrollbar and the trailing "Show all →" link stays anchored below.
  • Widget titles renamed to SuiteCRM: <Module> (colon-separated) matching Forgejo's naming convention — SuiteCRM: Cases, SuiteCRM: Pipeline, etc.
  • Dropped IAPIWidget / IAPIWidgetV2 from all nine widget classes so the Vue shell renders instead of NC's server-side WidgetItem card. The Vue shell is what carries the 3-dot toolbar and settings modal; NC's own WidgetItem card doesn't have a slot for those. The getItems* / getItemsV2* methods are kept as inert dead code so re-declaring the interface is a one-line change if a future NC version's dashboard app changes its precedence logic.
  • Built js/ bundles are now committed to git so a git checkout v<tag> on a deploy host lands a runnable app without needing node/npm on the host. Matches integration_forgejo_gitea's convention. Regenerate with npm run build before any release commit.

Notes for maintainers

  • Nextcloud release cadence: whenever a new Nextcloud major ships, bump <nextcloud max-version> in appinfo/info.xml, cross-check the PHP min-version against the new server's system-requirements page, and mirror any PHP floor change into composer.json (require and config.platform). A mismatch between the two is silent until someone tries to build on a workstation with an older PHP.
  • Verify the version-bearing files stay in lock-step before commit: appinfo/info.xml, package.json, package-lock.json, CHANGELOG.md, and the git tag. The npm run verify:version hook covers package.json vs appinfo/info.xml; the rest is manual review at commit time.
  • Icon file convention: img/app.svg + img/app-dark.svg are for the Nextcloud app-menu (picked up by NC by filename, mono contract). img/app-color.svg is the brand mark used inside the app's own widgets and settings pages. Do not repoint img/app.svg to the color asset — the app-menu contract expects mono ink that flows with the theme.
  • OWASP review baseline: 3.1.0 clears 0 High + 0 Medium findings on a post-iteration re-review. Every previously-fixed Medium/Low from the first pass is still applied. Four remaining Low advisories are documented tradeoffs (SuiteCRM ACL delegation on onlyMine=false, sensitive column NC-30 whitelist, committed js/ supply-chain note, reference-provider fan-out on paste). Any future write endpoint added to SuiteCRMAPIController needs its own #[UserRateLimit] attribute or an explicit reason in the class doc why it does not. Any future admin-writable config key needs a line in ADMIN_ALLOWED_KEYS and, if it holds a secret, an entry in the migration's SENSITIVE_APPCONFIG_KEYS. Any future widget-list endpoint needs $limit = $this->clampLimit($limit) before hitting the service.
  • New per-widget pref keys must be added to USER_ALLOWED_KEYS in ConfigController AND to the getWidgetConfig() return object AND to the widget's own Vue loadWidgetConfig() reader. Missing any one of the three silently drops the value from the read path or the save path.
  • Widget shell contract: SuiteCRMWidgetShell.vue emits refresh (user hit the refresh action) and save({ refreshSeconds, onlyMine, maxItems, extras, close }) (user hit Save in the modal). Callers must invoke close() inside their save flow — the shell does not auto-close so the caller can leave the modal open if the save failed and it wants the user to retry without re-typing.
  • Regression call-out: the previous "Follow-up Task from a calendar item" action (README line 55) is not currently reachable in the dashboard UI after the Vue widget rewrite; the modal component TaskFollowupModal.vue and the PHP createFollowupTask endpoint are still live but not wired. Re-adding this as a per-row action in the shell is a 3.2.0 scope item.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignaturepXX3njPlrc5StGaxF2bb6qjBDTWnAuMA/SV2M86Wv2linGxlX0aXL8plrixqrBIq
EQwOIaYFeXQM3k7kiIoYQhYcndlKXFRKoN6v1vo63zao/Zi2xlM6V7UoRGzsmBv7
ACiQo70Ia2ETG8ZF8dVUUlwFZ5Pq5ipccgjQe6EgOVu9fwmJQ+NDdhK4LUo4STRZ
QGsuApeNo7G2QyhsPfj+BkgZ1saicpqT8i0eG7OUBPfiyaB3YvkqV2nl6U3ocDcZ
NgbtkRPQCaIa2JymEzQTchZX/HbEqYrPJgaPd/VvMXTrd59fMz0serAevOZROdNJ
zGz+cx846ow83NdyOK931XIgINi6RWY9f2vtfdJCaxKen4mxtOHDXg0PmII1qJCl
gKDgMu3bOKs3RwtSeqPG3TOSN49PCmB4Wj6nM6QihA/dTpgHOSQ2JmpxKE/Zae/j
8Hmh23juWe4diixVnK4+oxzqfSBS4bqHTMtEsS6L5VDpnBYc75CK2w6vafj4xZ+/
CwlZbhX9T7tVYT9pqrazuBn1FzTm3xeHonKxRGt3OzfIPkM/ehA15ESGvldV57NG
G34UMXHhiijPT6fH0U5cARByHDcwWZSam1Iv+SwfZ8ysKZtQzb2NDrDwEpIfchZu
83qX8vpXhVD3KqFj4PS+MwI/rQ66Fh0hHoD+rBpheNA=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.0.2
Release Details
UpdatedJuly 29, 2026, 2:54 p.m.
Changelog

App Store metadata refresh, take two. The 3.0.1 upload was rejected by the App Store validator with Element 'summary': [facet 'maxLength'] The value has a length of '166'; this exceeds the allowed maximum length of '128'., so this release trims <summary> under the 128-character cap while keeping the widget-count and coverage cue that made the new listing useful in the first place.

Changed

  • appinfo/info.xml <summary> shortened from 166 to 117 characters to satisfy the App Store's apps/info.xsd maxLength facet on <summary>. New form drops the module enumeration and keeps the "nine widgets" specificity: "Nine dashboard widgets, unified search and notifications for SuiteCRM 8.x. Calendar, tasks, cases, pipeline and more." The full nine-widget breakdown is still in <description>, which the schema does not cap.

Notes for maintainers

The apps/info.xsd caps to remember when editing the top of appinfo/info.xml: - <name> max 40 characters - <summary> max 128 characters - <description> no practical cap (Markdown-friendly, use it for the full pitch)

Add a wc -c check on the summary line to release-nc-app.sh phase 1 to catch this locally next time.

Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignatureX4/pzvjKgONWyCtn4SRPhCb11mYQF8NyrEtydiOD9dttFgiLxLwcYhEQJfdBKLxY
KiG0c53jkSZHI0osL7UoSRWBu61KYv5AoRHS9ajdJSXD7xXR46nl56LH//teun3+
tbLSa2YsyNbIlcXDy8/ZeMrNApHk0HkEi9N8yv/m+sPERK975Oy8CrmgfGk7afuW
AkCY1NuDvZL4I5Dstt30Z6BW94hyiYXtky2W+voCjB2nAd6P/k5gGShDw61BrZnf
q1Y3moshtF/XV5niYLsvsj55mL24E8x22/VeMCYzQMKQtAahO8dcl9YrZ5rqho/b
NrxOFNrRVboUIZu5LSkHJgGC8y4GFwKUeyWtzKK6qUESaGU7qZlgi78ISdrh09yP
RLCmZBdE0QLOK0ZZ+Y9wWrq4/3+eE+5QFGJ3Itc4kvvbw80NIOhFL74NzAH15CuP
AFZ6EHUKNpqZ/9Ek0ppmRAaYrPnBsVqSCvDnRFVL8a3mi6Beul1B2Tixpx0wOKzd
wy5nx/eXEwBXBZJY91CgQDUFNMKMflxlIXgUIpAWUAjEemKlUJojxcmbB3+oNUPz
t8XTqPkcKedrDnLBdmyai6G2CTT187b6gx3u+qX2GT0HY1NG1x9PpH6+0ubL3loP
5GyLf0FkVjBVMgCVx08wRCnYWkTokDobVeDrHDkFiDo=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<35.0.0
Minimum Integer bits32
PHP>=8.2.0
SuiteCRM integration 3.0.0
Release Details
UpdatedJuly 29, 2026, 1:38 p.m.
Changelog

Renames the Nextcloud app id from njordium_suitecrm back to integration_suitecrm. This is the only breaking change in the release. Every setting on your existing 2.x install, admin OAuth config (instance URL, client ID/secret, authorize path), every per-user OAuth token, every widget preference (pipeline_mode, quick_actions_enabled, calendar_show_tasks) carries across automatically via a Repair step that runs on occ upgrade. Users do not need to re-authorise SuiteCRM.

Why the rename. When the fork first shipped in 2.0.0, Julien Veyssier's original integration_suitecrm App Store record was stale and blocked our updates from reaching his ~200 existing installs. The pragmatic response was to rename the fork's app id to njordium_suitecrm so we could ship on the App Store under our own record. In July 2026, Julien transferred ownership of the original record to this fork (see nextcloud/app-certificate-requests#1104 and #1114), which removed the original blocker. 3.0.0 restores the canonical integration_suitecrm app id so:

  • Julien's existing 1.x installs get updates seamlessly from the App Store, no manual reinstall.
  • There's one canonical id on the store, no user confusion about which of two records to install.
  • The GitHub org stays njordium/integration_suitecrm — the App Store app id and the GitHub org name are decoupled.

See docs/upgrade-2.x-to-3.0.md for the end-to-end upgrade walkthrough, rollback procedure, and post-upgrade verification steps.

Changed

  • App id renamed from njordium_suitecrm back to integration_suitecrm. The install folder is now custom_apps/integration_suitecrm/; the old custom_apps/njordium_suitecrm/ will no longer be recognised. Every HTTP route the frontend calls (/apps/integration_suitecrm/*) and every internal string reference has been updated in a single atomic pass across 35 files.
  • occ command name is now occ integration_suitecrm:test-connection. The 2.x form occ njordium_suitecrm:test-connection no longer resolves.
  • OAuth redirect URL on the SuiteCRM OAuth2 client must be updated from <nextcloud>/apps/njordium_suitecrm/oauth-callback to <nextcloud>/apps/integration_suitecrm/oauth-callback. Byte-for-byte match required. Same manual step users hit on the 1.9 to 2.0 upgrade; instructions in docs/upgrade-2.x-to-3.0.md.
  • Webpack bundle filenames flipped from njordium_suitecrm-*.js back to integration_suitecrm-*.js. Direct-install admins with cached JS in a reverse proxy should invalidate the cache on upgrade.
  • Migration\CopyLegacyAppConfig SQL body unchanged, LEGACY_APP_ID constant flipped from integration_suitecrm (2.0.0) to njordium_suitecrm (3.0.0). The class continues to run on every occ upgrade as a post-migration Repair step. On a fresh install (no rows to copy) it's a silent no-op.

Migration path

  • From 2.x (any 2.0-2.6 release) to 3.0.0: occ upgrade runs Migration\CopyLegacyAppConfig which copies every oc_appconfig row where appid='njordium_suitecrm' and every oc_preferences row for the same app id into appid='integration_suitecrm'. Legacy rows are left in place so rollback stays trivial. Encrypted OAuth tokens survive the copy because they live in oc_preferences under app id-scoped rows.
  • From 1.9.x directly to 3.0.0: oc_appconfig and oc_preferences rows are already under integration_suitecrm (that was Julien's app id), so the Repair step is a silent no-op and the deployment picks the settings up under the same id automatically.

Rollback

If 3.0.0 misbehaves, rollback to 2.6.0 is safe because the migration is copy-only, not move: occ app:disable integration_suitecrm && occ app:enable njordium_suitecrm && occ upgrade restores every setting. Detailed steps in docs/upgrade-2.x-to-3.0.md.

Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
Signatureogxzom4kGvfPpSAHfscVLCco55DbReJH1/78dRCmY1oXJ2XiHb1LEXA33NKJMSEt
OBKUO3MKw31GLqvjOSh/q1nIThsgvMviEnvqSHKf3A3Xd9zeJCW0gtuQsFrFGrIV
qsw16PpfY8qwa/WSpV7i3hVEY38BohCGP+GtlTiTg4OZUouyBkt7c5xqO6PlhXBX
fkIJepB1Lf6NLFa9B67Lxuzn0LLXG9BHamrfhCMvENcoJ3gfEXlbqB8/Isxg7i9D
hzgQD+pR/l6nNgJVwT8oatJpeS4SiarOH2RTMxSIkbBU1d9GgXTRSEZ2zLmU1/do
R2W9vcNWnLfNYpIzWVy9UQNG/OLd7aGGMp6Q5EJSKPQGleKLlCzoVwyb7ghNu6fU
NDeEpJQu9wNmCTEWJKzXrDiENJKAAoWHocLv5CL00xK/yGRTXj3of6NOmUfDkAq5
TfHMQcxnpj7PG103qqaZI/JQ3X49MXDbe/BOnXafNyhEXJVReYHTqXfSk+HTE4dm
VwJjN8cjunijsld5PLDWY3xyK8s/or+5xgikAwm5apcI/Masfk/FGsPWq4NJUXQN
VsQOX5KwNrGdPZmxxMdd5Rov8CkC9eE/Yrtwhem8fd1vs8b0T88PPv/224Fwb4jJ
oI9709FIemcZF3xrQTMPnwOmfKIVKiGVlOsva3IbYl8=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<35.0.0
Minimum Integer bits32
PHP>=8.2.0

Nextcloud 32

SuiteCRM integration 3.2.4
Release Details
UpdatedAug. 23, 2026, 8:33 a.m.
Changelog

Deprecation cleanup.

  • Notifier::prepare() now throws \OCP\Notification\UnknownNotificationException instead of \InvalidArgumentException when a notification isn't ours (or is a reminder for an unknown SuiteCRM module). Since Nextcloud 30, \InvalidArgumentException is the deprecated contract and every /ocs/v2.php/apps/notifications poll emitted a level-2 warning naming this notifier; the new class is the NC-30+ replacement. Behaviour is identical (row still suppressed on unknown subject); the warning line stops.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
Signatureg0H49K2pxy0oLASZymk71eZc/SqJV/l4y3hxSShrXRSFql30MsfhwCsKajn1AhPWcbu2y/hnFHf/SToF3IqVyiRLitcd80FgetIzgfyMGrzIyvzvNCngZKf6IHATuMu45deRGUp760yX51KsL9EGFDlbtiZpc1OeazbDpWfpBvqISRXhd0KEo6jhpLZCT9N3kPUOo4DbGzrq6PA3JbJA0zUuYXKoFAMGKvxyhm2TgYP7Q+ET7pkYpV4EVytSbNwBcfV5PPjAG55MdHM2Mg4Y22ppJraR2s7vGec3wl/iX6Oyf2jsjVj4F6In4wI6A+QWllyD4rVzvQ6YOvwTtaHJYckoKcTIs7RpfJiAgjHkzYnVVXnPdXLlBCt7rlCqxSBkqhkQngdobjAAuyYSr1bV+JySSXAH2n6vskEzSC+FNkz5TgzAqMcc8I0VOVpGpDhSpgUb4bbbX8gviMEEWQQu1tUuYFFUVqQcy9Zpt1jF/HcOBEL1k5sg1hHsk6r1DqB75fPqGOugxiNsEMa73iYiaV0HDUrMOybGtLkzDxTxRShJk/zN6D03oUSBAE1gaebu4mjvlz/KWvyh++nJH6AnIhiUbxgPfwTGXFN64qQ1zeKSuSUXrijLjaZ0BE2RQtXUvPjgJlq5V+txa/rNCL5ThHkn0yMiLcqhJB4QEq3P0Ig=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.2.3
Release Details
UpdatedAug. 15, 2026, 4:27 p.m.
Changelog

Admin-panel copy polish.

  • "OAuth authorize endpoint path" label shortened to "Authorize path" so it lines up visually with the other short-form labels ("Instance address", "OAuth client ID / secret", "Redirect URI") in the harmonised grid layout.
  • The trailing helper text ("SuiteCRM 8.10.x default: /Api/authorize. Older installs may use /legacy/oauth2/authorize.") is removed. The field still pre-fills with /Api/authorize on a fresh install via Admin.php's getValueString(..., '/Api/authorize') default, so an admin who never had to touch this row still sees the correct value.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignatureD/6aAuPWTF4VDuLA6GAOcjkW5AQK964/Pp8pVssyhws6pCt4Lpm4KHiN8djxiWA/VRCvLGtN70XmSYkJHBMZi1c6f4J2hawz/QmI+5GV03Y28c3oEt7rVSzNxOk7Q6mugRvyodjc0ar+eoiMPOXLuQoMQhjlUpNyIwILBL5hRVftviknUoP2FwbQeKIecc6biii7QqYtiOy+/s8o1PQRBQSE9/UiHvVEQeKHOWGVWX6ny3bHkzvEASQcWMStsfPR7eaEGZP8zOOeCrchinzdztxxogh0i0NCb/5D/4wao0gn8KNfTo7rtI1/TzjxN/eqgLHm2vd9uXYNk+cT0R51wqESAJrS2xPdYLsBWi6MJUyjXTLTP3ZAhvhYrO3XUqTL6QLDSt0wFGaqXx+xw7OYU3ryl8Ox3hMnhOKs5VuiMc9KSEcGPJ+GY18RyIJFvOLV02rYPdmboeSCiD2/n4beIrdMmfhiyErDmAaO0lo2hoA371UP6ghSorgH/aax4LdSYzJ37Ua8Ssm+nvq2NK9KqZXcPoupmBlAojq3o22oVfQcYr49p57af3pFBd6/MPl2 5sI5is0nhBuVMo6jLghs9ShMWMg2QAYsrTxLNoPiRmfqDNCPl0Z+x+9Rg5dgq2g2YRaEC1cfQEi4p0938FrE+LI02zlBeLokZ8l0/fTysRA=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.1.0
Release Details
UpdatedAug. 14, 2026, 7:50 p.m.
Changelog

Three cross-cutting changes go out in one tag: the Nextcloud 35 compatibility bump, color-brand widget icons across every dashboard surface, and the fixes surfaced by a full OWASP-Top-10 secure-coding review. Nothing changes for the end user's day-to-day interaction with the widgets, but the App Store now offers the update to Nextcloud 35 installs, admins see the SuiteCRM brand mark instead of a mono glyph on the dashboard and settings pages, and the security posture picks up rate limits on the four write endpoints, mime-pinning on the avatar proxy, an admin-config whitelist, plaintext-leak fixes on the OAuth log, and a hardening pass on the 2.x → 3.x config migration so client_secret no longer downgrades to admin-inspectable plaintext on upgrade.

Existing installs on 30–34 continue to work unchanged. Admins on PHP 8.2 need to upgrade PHP before installing this release.

Nextcloud 35 compatibility

  • appinfo/info.xml <nextcloud max-version> bumped from 34 to 35 so the App Store lists this release as compatible with the Nextcloud 35 line. Minimum stays at 30 — the widget code, dashboard API, notification and search integrations all use surface that has been stable across 30–35, so there is no reason to raise the floor.
  • appinfo/info.xml <php min-version> bumped from 8.2 to 8.3 to match Nextcloud 35's own PHP baseline. Nextcloud 35 will not boot on PHP 8.2, so advertising 8.2 support would let admins install the app onto a host that cannot actually run the server hosting it.
  • composer.json PHP constraint lifted to ^8.3 in both require and the config.platform block so composer install resolves against the same platform Nextcloud 35 runs on. The config.platform pin matters for anyone building the release tarball on a workstation that still has PHP 8.2 available — without it, composer would happily select older dependency versions than the ones the server actually loads.
  • appinfo/info.xml <author> order reordered so Kim Haverblad (current maintainer, homepage="https://njordium.com") appears above Julien Veyssier (original 1.x author). The App Store renders <author> elements in document order in the sidebar of apps.nextcloud.com/apps/integration_suitecrm; leading with the maintainer who ships the releases matches what users see in the App Store listing for the release they are actually installing. Julien's contribution is still credited — this is an ordering change, not a removal.

Color widget brand icon

Mirrors the pattern shipped in the sibling integration_forgejo_gitea app, which uses a single brand-color SVG across every dashboard widget header and the admin/personal settings anchor. The img/app-color.svg asset was already in the repository from earlier work but not wired anywhere.

  • css/dashboard.css .icon-suitecrm collapsed from the two-rule mono theme-swap (app-dark.svg on light theme, app.svg on dark theme) to a single rule pointing at img/app-color.svg. The color logo reads well against both themes, so no body.theme--dark override is needed. All nine dashboard widget classes (SuiteCRMWidget, SuiteCRMCalendarWidget, SuiteCRMTasksWidget, SuiteCRMCasesWidget, SuiteCRMPipelineWidget, SuiteCRMActivitiesWidget, SuiteCRMContactsWidget, SuiteCRMAccountsWidget, SuiteCRMLeadsWidget) pick this up through their existing getIconClass(): return 'icon-suitecrm' return, so no per-widget PHP change was needed for the CSS layer.
  • Each widget's getIconUrl() now returns the color asset instead of the mono app.svg. The Nextcloud 30+ dashboard app prefers getIconUrl() over getIconClass() because it renders the icon outside the app's own stylesheet context (mail digests, dashboard permalinks); the two paths now agree.
  • SuiteCRMWidget::iconForModule() and SuiteCRMCalendarWidget::iconForModule() per-item icon fallback (the icon shown next to a row when the module is not Calls/Meetings) is now app-color.svg so a Task or Note row carries the brand mark rather than the mono glyph.
  • Vue settings anchorssrc/components/PersonalSettings.vue and src/components/AdminSettings.vue inline the same collapsed rule so the little icon next to the "Connected accounts" / admin settings heading matches the dashboard widgets.
  • Vue view avatar fallback — the seven views that hand SuiteCRM rows to NcListItem (Activities.vue, Cases.vue, Tasks.vue, Pipeline.vue, RecentContacts.vue, RecentAccounts.vue, RecentLeads.vue) now fall back to app-color.svg instead of app.svg when a row has no per-record avatar, so the visual grammar is consistent across the whole app surface.
  • App-menu icons (img/app.svg, img/app-dark.svg) are unchanged. Nextcloud picks those up by file convention for the top navigation bar, where the mono contract still applies — same split the Forgejo/Gitea app uses.

Secure-coding review fixes (OWASP Top-10 pass)

Ran a full static analysis over lib/, src/, templates/, and appinfo/ against the OWASP Top-10 (2021) taxonomy. Zero High findings. Three Medium and four Low findings are fixed in this release; the JWT-signature note (A02) is documented as accepted-risk in the source and needs no code change.

  • A02 Medium — Migration\CopyLegacyAppConfig copied oc_appconfig rows via a raw QueryBuilder INSERT of (appid, configkey, configvalue), which left the sensitive column at its table default of false. Any 2.x install upgrading to 3.x therefore had its client_secret land under the new app id as admin-inspectable plaintext — visible in occ config:app:get integration_suitecrm client_secret and in Nextcloud support-bundle dumps. Fix routes the write through IAppConfig::setValueString with an explicit sensitive: argument, derived from a new SENSITIVE_APPCONFIG_KEYS class-const whitelist. A structural regression test (CopyLegacyAppConfigTest::testAppConfigCopyPreservesSensitiveFlag) locks the guarantee. The migration only selects configkey + configvalue; the sensitive column was added to oc_appconfig in Nextcloud 31, and selecting it on Nextcloud 30 aborts the entire repair step with SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sensitive'. The whitelist alone gives the same practical protection because client_secret is the only sensitive key this app persists.
  • A04 Medium — write endpoints unbounded. The four #[NoAdminRequired] POST endpoints createFollowupTask, logNote, linkDeckCard, and emailToCase all previously accepted requests one-per-round-trip with no rate ceiling. A compromised NC user session — or a cross-app XSS finding a hole elsewhere in the install — could flood the connected SuiteCRM with thousands of Notes / Tasks / Cases per second, up to the SuiteCRM ACL boundary. Each endpoint now carries #[UserRateThrottle(limit: 30, period: 60)] — 30 writes per minute per user, more than enough for a real human workflow, several orders of magnitude below any abuse curve.
  • A05 Low — avatar Content-Type/cache. getSuiteCRMAvatar() is #[NoCSRFRequired] (so it can be embedded in an <img> src) and cached for 24 hours. The DataDisplayResponse default Content-Type of application/octet-stream combined with a compromised or misconfigured SuiteCRM upstream returning an HTML/SVG blob would let an attacker land a cached blob at the Nextcloud origin. Fix magic-byte-sniffs the payload against a whitelist of JPEG/PNG/GIF/WebP (SVG is deliberately excluded because it can carry <script>) and pins the response Content-Type to the sniffed mime; anything that does not sniff as a real image is served as an empty image/png body. nosniff is already set globally by Nextcloud; this closes the hole for browsers that ignore it.
  • A01 Low — setAdminConfig() accepted arbitrary keys. Mirrored the existing USER_ALLOWED_KEYS pattern with an ADMIN_ALLOWED_KEYS whitelist covering the four keys the admin form actually writes (oauth_instance_url, client_id, client_secret, oauth_authorize_path). Silently drops unknown keys, same permissive-drop model as the user-side endpoint.
  • A09 Low — OAuth error log leaked raw guzzle message. The SuiteCRM OAuth exchange failed log line included 'raw' => $result['error'], which on Guzzle client-error paths can embed the request URL and therefore the one-time authorization code= query parameter. The code is already consumed by the time it hits the log, but log hygiene principle is to redact. Removed the field; the structured http_status / error_code / error_description / error_kind context already carries the actionable diagnostic.
  • Non-OWASP hygiene — URL-encoding consistency. SuiteCRMAPIService.php line 110 built the reminder notification click-through as a raw concat of $suitecrmUrl . '/index.php?module=' . $module . '&action=DetailView&record=' . $elemId. Two sibling call sites (SuiteCRMWidget::buildEventLink() and SuiteCRMCalendarWidget::buildEventLink()) already rawurlencode both parts; the inconsistency was a maintenance trap even though the values are TLS-fetched from the admin-configured SuiteCRM. Third call site now matches.

Widget UX overhaul — 3-dot per-widget settings

Aligns the SuiteCRM widget UX with the sibling integration_forgejo_gitea app: every widget now carries a 3-dot toolbar menu with a Refresh action and a Widget settings modal. Per-user refresh cadence, records-to-show count, and (for eight of the nine widgets) "Only records assigned to me" toggle are stored per widget so users can tune each widget independently.

Settings modal exposes three (four for Calendar / Pipeline) sections per widget: - Refresh frequency — Never / 30s / 1m / 5m / 15m / 30m / 1h (default 5 minutes) - Records to show — 5 / 10 / 15 / 20 / 25 / 50 (default 20). Doubles as the fetch limit sent to the SuiteCRM API so we don't ask for rows we'd drop client-side; server clamps to max(1, min(100, N)) as defence against a tampered client. - Show → Only records assigned to me — checkbox on eight of nine widgets. Defaults ON for Calendar / Cases / Tasks / Pipeline (matches the existing single-user behavior; toggle OFF widens to team-visible records within your SuiteCRM ACL). Defaults OFF for Activities / Contacts / Accounts / Leads (they've always shown any recently-added record; toggle ON filters to your own). Events (reminders) is per-user by SuiteCRM design and has no toggle. - Widget-specific extras — Calendar carries "Include Tasks alongside Meetings and Calls"; Pipeline carries the "Closing this quarter / Top value / Weighted value" framing radio (moved out of PersonalSettings.vue's global block).

  • Widget titles are now SuiteCRM: <Module> (colon-separated), matching the Forgejo widget naming convention. All nine widgets: SuiteCRM: Events, SuiteCRM: Calendar, SuiteCRM: Tasks, SuiteCRM: Cases, SuiteCRM: Pipeline, SuiteCRM: Activities, SuiteCRM: Contacts, SuiteCRM: Accounts, SuiteCRM: Leads.
  • Shared SuiteCRMWidgetShell.vue component replaces the per-widget NcDashboardWidget wrapping. The shell renders the toolbar, the loading / not-connected / error / empty states, the slot for the widget's item list, and the settings modal. The Forgejo sibling inlines this shape per widget; we chose to abstract it here because SuiteCRM has nine widgets that share the same shell — duplicating the toolbar and modal nine times would have been 200+ lines of copy-paste with no functional payoff. The visible UX matches Forgejo exactly: same NcActions layout, same modal structure, same RefreshIntervalPicker behaviour.
  • Ported useAutoRefresh.js composable and RefreshIntervalPicker.vue component verbatim from the Forgejo app (with the app id string swapped) so both apps share the same polling semantics — timer plus wake-signal refetches on tab focus, visibility change, and bfcache restore. setIntervalMs(0) disables the timer, wake-signal refetches still run. Cadence options: never / 30s / 60s / 5min / 15min / 30min / 1h.
  • New per-user pref keys (whitelisted in ConfigController::USER_ALLOWED_KEYS): events_refresh_seconds, calendar_refresh_seconds, tasks_refresh_seconds, cases_refresh_seconds, pipeline_refresh_seconds, activities_refresh_seconds, contacts_refresh_seconds, accounts_refresh_seconds, leads_refresh_seconds. All default to 300s.
  • New GET /widget-config endpoint returns the current value of every widget refresh key plus calendar_show_tasks and pipeline_mode in one round-trip; each widget calls it on mount and passes the seed into useAutoRefresh.setIntervalMs(). Save flow reuses the existing PUT /config endpoint, so the AuthZ gate for widget settings is the same USER_ALLOWED_KEYS whitelist the personal-settings page has always used.
  • Per-widget settings moved out of PersonalSettings.vue and into the widget's own 3-dot menu where they belong:
  • Calendar widget: calendar_show_tasks toggle (include SuiteCRM Tasks alongside Meetings/Calls)
  • Pipeline widget: pipeline_mode radio selector (closing this quarter / top value / weighted) The keys themselves are unchanged, so a 3.0.x install upgrading to 3.1.0 keeps whatever value the user had picked.
  • Shared .scw-* CSS classes (SuiteCRM Widget prefix) added to css/dashboard.css for the item list styling — one place instead of nine scoped <style> blocks with the same declarations. css/dashboard.css was already loaded on every widget page via Util::addStyle() in each widget's load() hook, so no additional PHP registration was needed.
  • Semantic per-widget row icons (via vue-material-design-icons): AccountOutline for Contacts, Handshake for Leads, OfficeBuilding for Accounts, Briefcase for Cases, FormatListChecks for Tasks, TrendingUp for Pipeline. Activities / Calendar / Events dispatch per row type (CalendarClock for Meetings, PhoneOutline for Calls, FormatListChecks for Tasks, Note for Notes, BellRing fallback). Rendered inside a small circular container tinted with --color-primary-element so icons flow with the user's Nextcloud accent. Considered SuiteCRM's own SuitePicons font pack for brand consistency but rejected it (icon font, ~100KB with no CDN, glyph-map not published) — MDI is already installed and gives the same semantic clarity at ~200 bytes per icon.
  • CheckCircleOutline empty-state icon on every widget so a widget with zero results still reads as "you're all caught up" rather than a blank card.
  • List behaviour: capped at max-height: 320px with overflow-y: auto. When the user's picked records count fits, the widget is naturally that height; when it doesn't, the list scrolls internally with a subtle scrollbar and the trailing "Show all →" link stays anchored below.
  • Widget titles renamed to SuiteCRM: <Module> (colon-separated) matching Forgejo's naming convention — SuiteCRM: Cases, SuiteCRM: Pipeline, etc.
  • Dropped IAPIWidget / IAPIWidgetV2 from all nine widget classes so the Vue shell renders instead of NC's server-side WidgetItem card. The Vue shell is what carries the 3-dot toolbar and settings modal; NC's own WidgetItem card doesn't have a slot for those. The getItems* / getItemsV2* methods are kept as inert dead code so re-declaring the interface is a one-line change if a future NC version's dashboard app changes its precedence logic.
  • Built js/ bundles are now committed to git so a git checkout v<tag> on a deploy host lands a runnable app without needing node/npm on the host. Matches integration_forgejo_gitea's convention. Regenerate with npm run build before any release commit.

Notes for maintainers

  • Nextcloud release cadence: whenever a new Nextcloud major ships, bump <nextcloud max-version> in appinfo/info.xml, cross-check the PHP min-version against the new server's system-requirements page, and mirror any PHP floor change into composer.json (require and config.platform). A mismatch between the two is silent until someone tries to build on a workstation with an older PHP.
  • Verify the version-bearing files stay in lock-step before commit: appinfo/info.xml, package.json, package-lock.json, CHANGELOG.md, and the git tag. The npm run verify:version hook covers package.json vs appinfo/info.xml; the rest is manual review at commit time.
  • Icon file convention: img/app.svg + img/app-dark.svg are for the Nextcloud app-menu (picked up by NC by filename, mono contract). img/app-color.svg is the brand mark used inside the app's own widgets and settings pages. Do not repoint img/app.svg to the color asset — the app-menu contract expects mono ink that flows with the theme.
  • OWASP review baseline: 3.1.0 clears 0 High + 0 Medium findings on a post-iteration re-review. Every previously-fixed Medium/Low from the first pass is still applied. Four remaining Low advisories are documented tradeoffs (SuiteCRM ACL delegation on onlyMine=false, sensitive column NC-30 whitelist, committed js/ supply-chain note, reference-provider fan-out on paste). Any future write endpoint added to SuiteCRMAPIController needs its own #[UserRateLimit] attribute or an explicit reason in the class doc why it does not. Any future admin-writable config key needs a line in ADMIN_ALLOWED_KEYS and, if it holds a secret, an entry in the migration's SENSITIVE_APPCONFIG_KEYS. Any future widget-list endpoint needs $limit = $this->clampLimit($limit) before hitting the service.
  • New per-widget pref keys must be added to USER_ALLOWED_KEYS in ConfigController AND to the getWidgetConfig() return object AND to the widget's own Vue loadWidgetConfig() reader. Missing any one of the three silently drops the value from the read path or the save path.
  • Widget shell contract: SuiteCRMWidgetShell.vue emits refresh (user hit the refresh action) and save({ refreshSeconds, onlyMine, maxItems, extras, close }) (user hit Save in the modal). Callers must invoke close() inside their save flow — the shell does not auto-close so the caller can leave the modal open if the save failed and it wants the user to retry without re-typing.
  • Regression call-out: the previous "Follow-up Task from a calendar item" action (README line 55) is not currently reachable in the dashboard UI after the Vue widget rewrite; the modal component TaskFollowupModal.vue and the PHP createFollowupTask endpoint are still live but not wired. Re-adding this as a per-row action in the shell is a 3.2.0 scope item.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignaturepXX3njPlrc5StGaxF2bb6qjBDTWnAuMA/SV2M86Wv2linGxlX0aXL8plrixqrBIq
EQwOIaYFeXQM3k7kiIoYQhYcndlKXFRKoN6v1vo63zao/Zi2xlM6V7UoRGzsmBv7
ACiQo70Ia2ETG8ZF8dVUUlwFZ5Pq5ipccgjQe6EgOVu9fwmJQ+NDdhK4LUo4STRZ
QGsuApeNo7G2QyhsPfj+BkgZ1saicpqT8i0eG7OUBPfiyaB3YvkqV2nl6U3ocDcZ
NgbtkRPQCaIa2JymEzQTchZX/HbEqYrPJgaPd/VvMXTrd59fMz0serAevOZROdNJ
zGz+cx846ow83NdyOK931XIgINi6RWY9f2vtfdJCaxKen4mxtOHDXg0PmII1qJCl
gKDgMu3bOKs3RwtSeqPG3TOSN49PCmB4Wj6nM6QihA/dTpgHOSQ2JmpxKE/Zae/j
8Hmh23juWe4diixVnK4+oxzqfSBS4bqHTMtEsS6L5VDpnBYc75CK2w6vafj4xZ+/
CwlZbhX9T7tVYT9pqrazuBn1FzTm3xeHonKxRGt3OzfIPkM/ehA15ESGvldV57NG
G34UMXHhiijPT6fH0U5cARByHDcwWZSam1Iv+SwfZ8ysKZtQzb2NDrDwEpIfchZu
83qX8vpXhVD3KqFj4PS+MwI/rQ66Fh0hHoD+rBpheNA=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.0.2
Release Details
UpdatedJuly 29, 2026, 2:54 p.m.
Changelog

App Store metadata refresh, take two. The 3.0.1 upload was rejected by the App Store validator with Element 'summary': [facet 'maxLength'] The value has a length of '166'; this exceeds the allowed maximum length of '128'., so this release trims <summary> under the 128-character cap while keeping the widget-count and coverage cue that made the new listing useful in the first place.

Changed

  • appinfo/info.xml <summary> shortened from 166 to 117 characters to satisfy the App Store's apps/info.xsd maxLength facet on <summary>. New form drops the module enumeration and keeps the "nine widgets" specificity: "Nine dashboard widgets, unified search and notifications for SuiteCRM 8.x. Calendar, tasks, cases, pipeline and more." The full nine-widget breakdown is still in <description>, which the schema does not cap.

Notes for maintainers

The apps/info.xsd caps to remember when editing the top of appinfo/info.xml: - <name> max 40 characters - <summary> max 128 characters - <description> no practical cap (Markdown-friendly, use it for the full pitch)

Add a wc -c check on the summary line to release-nc-app.sh phase 1 to catch this locally next time.

Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignatureX4/pzvjKgONWyCtn4SRPhCb11mYQF8NyrEtydiOD9dttFgiLxLwcYhEQJfdBKLxY
KiG0c53jkSZHI0osL7UoSRWBu61KYv5AoRHS9ajdJSXD7xXR46nl56LH//teun3+
tbLSa2YsyNbIlcXDy8/ZeMrNApHk0HkEi9N8yv/m+sPERK975Oy8CrmgfGk7afuW
AkCY1NuDvZL4I5Dstt30Z6BW94hyiYXtky2W+voCjB2nAd6P/k5gGShDw61BrZnf
q1Y3moshtF/XV5niYLsvsj55mL24E8x22/VeMCYzQMKQtAahO8dcl9YrZ5rqho/b
NrxOFNrRVboUIZu5LSkHJgGC8y4GFwKUeyWtzKK6qUESaGU7qZlgi78ISdrh09yP
RLCmZBdE0QLOK0ZZ+Y9wWrq4/3+eE+5QFGJ3Itc4kvvbw80NIOhFL74NzAH15CuP
AFZ6EHUKNpqZ/9Ek0ppmRAaYrPnBsVqSCvDnRFVL8a3mi6Beul1B2Tixpx0wOKzd
wy5nx/eXEwBXBZJY91CgQDUFNMKMflxlIXgUIpAWUAjEemKlUJojxcmbB3+oNUPz
t8XTqPkcKedrDnLBdmyai6G2CTT187b6gx3u+qX2GT0HY1NG1x9PpH6+0ubL3loP
5GyLf0FkVjBVMgCVx08wRCnYWkTokDobVeDrHDkFiDo=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<35.0.0
Minimum Integer bits32
PHP>=8.2.0
SuiteCRM integration 3.0.0
Release Details
UpdatedJuly 29, 2026, 1:38 p.m.
Changelog

Renames the Nextcloud app id from njordium_suitecrm back to integration_suitecrm. This is the only breaking change in the release. Every setting on your existing 2.x install, admin OAuth config (instance URL, client ID/secret, authorize path), every per-user OAuth token, every widget preference (pipeline_mode, quick_actions_enabled, calendar_show_tasks) carries across automatically via a Repair step that runs on occ upgrade. Users do not need to re-authorise SuiteCRM.

Why the rename. When the fork first shipped in 2.0.0, Julien Veyssier's original integration_suitecrm App Store record was stale and blocked our updates from reaching his ~200 existing installs. The pragmatic response was to rename the fork's app id to njordium_suitecrm so we could ship on the App Store under our own record. In July 2026, Julien transferred ownership of the original record to this fork (see nextcloud/app-certificate-requests#1104 and #1114), which removed the original blocker. 3.0.0 restores the canonical integration_suitecrm app id so:

  • Julien's existing 1.x installs get updates seamlessly from the App Store, no manual reinstall.
  • There's one canonical id on the store, no user confusion about which of two records to install.
  • The GitHub org stays njordium/integration_suitecrm — the App Store app id and the GitHub org name are decoupled.

See docs/upgrade-2.x-to-3.0.md for the end-to-end upgrade walkthrough, rollback procedure, and post-upgrade verification steps.

Changed

  • App id renamed from njordium_suitecrm back to integration_suitecrm. The install folder is now custom_apps/integration_suitecrm/; the old custom_apps/njordium_suitecrm/ will no longer be recognised. Every HTTP route the frontend calls (/apps/integration_suitecrm/*) and every internal string reference has been updated in a single atomic pass across 35 files.
  • occ command name is now occ integration_suitecrm:test-connection. The 2.x form occ njordium_suitecrm:test-connection no longer resolves.
  • OAuth redirect URL on the SuiteCRM OAuth2 client must be updated from <nextcloud>/apps/njordium_suitecrm/oauth-callback to <nextcloud>/apps/integration_suitecrm/oauth-callback. Byte-for-byte match required. Same manual step users hit on the 1.9 to 2.0 upgrade; instructions in docs/upgrade-2.x-to-3.0.md.
  • Webpack bundle filenames flipped from njordium_suitecrm-*.js back to integration_suitecrm-*.js. Direct-install admins with cached JS in a reverse proxy should invalidate the cache on upgrade.
  • Migration\CopyLegacyAppConfig SQL body unchanged, LEGACY_APP_ID constant flipped from integration_suitecrm (2.0.0) to njordium_suitecrm (3.0.0). The class continues to run on every occ upgrade as a post-migration Repair step. On a fresh install (no rows to copy) it's a silent no-op.

Migration path

  • From 2.x (any 2.0-2.6 release) to 3.0.0: occ upgrade runs Migration\CopyLegacyAppConfig which copies every oc_appconfig row where appid='njordium_suitecrm' and every oc_preferences row for the same app id into appid='integration_suitecrm'. Legacy rows are left in place so rollback stays trivial. Encrypted OAuth tokens survive the copy because they live in oc_preferences under app id-scoped rows.
  • From 1.9.x directly to 3.0.0: oc_appconfig and oc_preferences rows are already under integration_suitecrm (that was Julien's app id), so the Repair step is a silent no-op and the deployment picks the settings up under the same id automatically.

Rollback

If 3.0.0 misbehaves, rollback to 2.6.0 is safe because the migration is copy-only, not move: occ app:disable integration_suitecrm && occ app:enable njordium_suitecrm && occ upgrade restores every setting. Detailed steps in docs/upgrade-2.x-to-3.0.md.

Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
Signatureogxzom4kGvfPpSAHfscVLCco55DbReJH1/78dRCmY1oXJ2XiHb1LEXA33NKJMSEt
OBKUO3MKw31GLqvjOSh/q1nIThsgvMviEnvqSHKf3A3Xd9zeJCW0gtuQsFrFGrIV
qsw16PpfY8qwa/WSpV7i3hVEY38BohCGP+GtlTiTg4OZUouyBkt7c5xqO6PlhXBX
fkIJepB1Lf6NLFa9B67Lxuzn0LLXG9BHamrfhCMvENcoJ3gfEXlbqB8/Isxg7i9D
hzgQD+pR/l6nNgJVwT8oatJpeS4SiarOH2RTMxSIkbBU1d9GgXTRSEZ2zLmU1/do
R2W9vcNWnLfNYpIzWVy9UQNG/OLd7aGGMp6Q5EJSKPQGleKLlCzoVwyb7ghNu6fU
NDeEpJQu9wNmCTEWJKzXrDiENJKAAoWHocLv5CL00xK/yGRTXj3of6NOmUfDkAq5
TfHMQcxnpj7PG103qqaZI/JQ3X49MXDbe/BOnXafNyhEXJVReYHTqXfSk+HTE4dm
VwJjN8cjunijsld5PLDWY3xyK8s/or+5xgikAwm5apcI/Masfk/FGsPWq4NJUXQN
VsQOX5KwNrGdPZmxxMdd5Rov8CkC9eE/Yrtwhem8fd1vs8b0T88PPv/224Fwb4jJ
oI9709FIemcZF3xrQTMPnwOmfKIVKiGVlOsva3IbYl8=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<35.0.0
Minimum Integer bits32
PHP>=8.2.0

Nextcloud 31

SuiteCRM integration 3.2.4
Release Details
UpdatedAug. 23, 2026, 8:33 a.m.
Changelog

Deprecation cleanup.

  • Notifier::prepare() now throws \OCP\Notification\UnknownNotificationException instead of \InvalidArgumentException when a notification isn't ours (or is a reminder for an unknown SuiteCRM module). Since Nextcloud 30, \InvalidArgumentException is the deprecated contract and every /ocs/v2.php/apps/notifications poll emitted a level-2 warning naming this notifier; the new class is the NC-30+ replacement. Behaviour is identical (row still suppressed on unknown subject); the warning line stops.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
Signatureg0H49K2pxy0oLASZymk71eZc/SqJV/l4y3hxSShrXRSFql30MsfhwCsKajn1AhPWcbu2y/hnFHf/SToF3IqVyiRLitcd80FgetIzgfyMGrzIyvzvNCngZKf6IHATuMu45deRGUp760yX51KsL9EGFDlbtiZpc1OeazbDpWfpBvqISRXhd0KEo6jhpLZCT9N3kPUOo4DbGzrq6PA3JbJA0zUuYXKoFAMGKvxyhm2TgYP7Q+ET7pkYpV4EVytSbNwBcfV5PPjAG55MdHM2Mg4Y22ppJraR2s7vGec3wl/iX6Oyf2jsjVj4F6In4wI6A+QWllyD4rVzvQ6YOvwTtaHJYckoKcTIs7RpfJiAgjHkzYnVVXnPdXLlBCt7rlCqxSBkqhkQngdobjAAuyYSr1bV+JySSXAH2n6vskEzSC+FNkz5TgzAqMcc8I0VOVpGpDhSpgUb4bbbX8gviMEEWQQu1tUuYFFUVqQcy9Zpt1jF/HcOBEL1k5sg1hHsk6r1DqB75fPqGOugxiNsEMa73iYiaV0HDUrMOybGtLkzDxTxRShJk/zN6D03oUSBAE1gaebu4mjvlz/KWvyh++nJH6AnIhiUbxgPfwTGXFN64qQ1zeKSuSUXrijLjaZ0BE2RQtXUvPjgJlq5V+txa/rNCL5ThHkn0yMiLcqhJB4QEq3P0Ig=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.2.3
Release Details
UpdatedAug. 15, 2026, 4:27 p.m.
Changelog

Admin-panel copy polish.

  • "OAuth authorize endpoint path" label shortened to "Authorize path" so it lines up visually with the other short-form labels ("Instance address", "OAuth client ID / secret", "Redirect URI") in the harmonised grid layout.
  • The trailing helper text ("SuiteCRM 8.10.x default: /Api/authorize. Older installs may use /legacy/oauth2/authorize.") is removed. The field still pre-fills with /Api/authorize on a fresh install via Admin.php's getValueString(..., '/Api/authorize') default, so an admin who never had to touch this row still sees the correct value.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignatureD/6aAuPWTF4VDuLA6GAOcjkW5AQK964/Pp8pVssyhws6pCt4Lpm4KHiN8djxiWA/VRCvLGtN70XmSYkJHBMZi1c6f4J2hawz/QmI+5GV03Y28c3oEt7rVSzNxOk7Q6mugRvyodjc0ar+eoiMPOXLuQoMQhjlUpNyIwILBL5hRVftviknUoP2FwbQeKIecc6biii7QqYtiOy+/s8o1PQRBQSE9/UiHvVEQeKHOWGVWX6ny3bHkzvEASQcWMStsfPR7eaEGZP8zOOeCrchinzdztxxogh0i0NCb/5D/4wao0gn8KNfTo7rtI1/TzjxN/eqgLHm2vd9uXYNk+cT0R51wqESAJrS2xPdYLsBWi6MJUyjXTLTP3ZAhvhYrO3XUqTL6QLDSt0wFGaqXx+xw7OYU3ryl8Ox3hMnhOKs5VuiMc9KSEcGPJ+GY18RyIJFvOLV02rYPdmboeSCiD2/n4beIrdMmfhiyErDmAaO0lo2hoA371UP6ghSorgH/aax4LdSYzJ37Ua8Ssm+nvq2NK9KqZXcPoupmBlAojq3o22oVfQcYr49p57af3pFBd6/MPl2 5sI5is0nhBuVMo6jLghs9ShMWMg2QAYsrTxLNoPiRmfqDNCPl0Z+x+9Rg5dgq2g2YRaEC1cfQEi4p0938FrE+LI02zlBeLokZ8l0/fTysRA=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.1.0
Release Details
UpdatedAug. 14, 2026, 7:50 p.m.
Changelog

Three cross-cutting changes go out in one tag: the Nextcloud 35 compatibility bump, color-brand widget icons across every dashboard surface, and the fixes surfaced by a full OWASP-Top-10 secure-coding review. Nothing changes for the end user's day-to-day interaction with the widgets, but the App Store now offers the update to Nextcloud 35 installs, admins see the SuiteCRM brand mark instead of a mono glyph on the dashboard and settings pages, and the security posture picks up rate limits on the four write endpoints, mime-pinning on the avatar proxy, an admin-config whitelist, plaintext-leak fixes on the OAuth log, and a hardening pass on the 2.x → 3.x config migration so client_secret no longer downgrades to admin-inspectable plaintext on upgrade.

Existing installs on 30–34 continue to work unchanged. Admins on PHP 8.2 need to upgrade PHP before installing this release.

Nextcloud 35 compatibility

  • appinfo/info.xml <nextcloud max-version> bumped from 34 to 35 so the App Store lists this release as compatible with the Nextcloud 35 line. Minimum stays at 30 — the widget code, dashboard API, notification and search integrations all use surface that has been stable across 30–35, so there is no reason to raise the floor.
  • appinfo/info.xml <php min-version> bumped from 8.2 to 8.3 to match Nextcloud 35's own PHP baseline. Nextcloud 35 will not boot on PHP 8.2, so advertising 8.2 support would let admins install the app onto a host that cannot actually run the server hosting it.
  • composer.json PHP constraint lifted to ^8.3 in both require and the config.platform block so composer install resolves against the same platform Nextcloud 35 runs on. The config.platform pin matters for anyone building the release tarball on a workstation that still has PHP 8.2 available — without it, composer would happily select older dependency versions than the ones the server actually loads.
  • appinfo/info.xml <author> order reordered so Kim Haverblad (current maintainer, homepage="https://njordium.com") appears above Julien Veyssier (original 1.x author). The App Store renders <author> elements in document order in the sidebar of apps.nextcloud.com/apps/integration_suitecrm; leading with the maintainer who ships the releases matches what users see in the App Store listing for the release they are actually installing. Julien's contribution is still credited — this is an ordering change, not a removal.

Color widget brand icon

Mirrors the pattern shipped in the sibling integration_forgejo_gitea app, which uses a single brand-color SVG across every dashboard widget header and the admin/personal settings anchor. The img/app-color.svg asset was already in the repository from earlier work but not wired anywhere.

  • css/dashboard.css .icon-suitecrm collapsed from the two-rule mono theme-swap (app-dark.svg on light theme, app.svg on dark theme) to a single rule pointing at img/app-color.svg. The color logo reads well against both themes, so no body.theme--dark override is needed. All nine dashboard widget classes (SuiteCRMWidget, SuiteCRMCalendarWidget, SuiteCRMTasksWidget, SuiteCRMCasesWidget, SuiteCRMPipelineWidget, SuiteCRMActivitiesWidget, SuiteCRMContactsWidget, SuiteCRMAccountsWidget, SuiteCRMLeadsWidget) pick this up through their existing getIconClass(): return 'icon-suitecrm' return, so no per-widget PHP change was needed for the CSS layer.
  • Each widget's getIconUrl() now returns the color asset instead of the mono app.svg. The Nextcloud 30+ dashboard app prefers getIconUrl() over getIconClass() because it renders the icon outside the app's own stylesheet context (mail digests, dashboard permalinks); the two paths now agree.
  • SuiteCRMWidget::iconForModule() and SuiteCRMCalendarWidget::iconForModule() per-item icon fallback (the icon shown next to a row when the module is not Calls/Meetings) is now app-color.svg so a Task or Note row carries the brand mark rather than the mono glyph.
  • Vue settings anchorssrc/components/PersonalSettings.vue and src/components/AdminSettings.vue inline the same collapsed rule so the little icon next to the "Connected accounts" / admin settings heading matches the dashboard widgets.
  • Vue view avatar fallback — the seven views that hand SuiteCRM rows to NcListItem (Activities.vue, Cases.vue, Tasks.vue, Pipeline.vue, RecentContacts.vue, RecentAccounts.vue, RecentLeads.vue) now fall back to app-color.svg instead of app.svg when a row has no per-record avatar, so the visual grammar is consistent across the whole app surface.
  • App-menu icons (img/app.svg, img/app-dark.svg) are unchanged. Nextcloud picks those up by file convention for the top navigation bar, where the mono contract still applies — same split the Forgejo/Gitea app uses.

Secure-coding review fixes (OWASP Top-10 pass)

Ran a full static analysis over lib/, src/, templates/, and appinfo/ against the OWASP Top-10 (2021) taxonomy. Zero High findings. Three Medium and four Low findings are fixed in this release; the JWT-signature note (A02) is documented as accepted-risk in the source and needs no code change.

  • A02 Medium — Migration\CopyLegacyAppConfig copied oc_appconfig rows via a raw QueryBuilder INSERT of (appid, configkey, configvalue), which left the sensitive column at its table default of false. Any 2.x install upgrading to 3.x therefore had its client_secret land under the new app id as admin-inspectable plaintext — visible in occ config:app:get integration_suitecrm client_secret and in Nextcloud support-bundle dumps. Fix routes the write through IAppConfig::setValueString with an explicit sensitive: argument, derived from a new SENSITIVE_APPCONFIG_KEYS class-const whitelist. A structural regression test (CopyLegacyAppConfigTest::testAppConfigCopyPreservesSensitiveFlag) locks the guarantee. The migration only selects configkey + configvalue; the sensitive column was added to oc_appconfig in Nextcloud 31, and selecting it on Nextcloud 30 aborts the entire repair step with SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sensitive'. The whitelist alone gives the same practical protection because client_secret is the only sensitive key this app persists.
  • A04 Medium — write endpoints unbounded. The four #[NoAdminRequired] POST endpoints createFollowupTask, logNote, linkDeckCard, and emailToCase all previously accepted requests one-per-round-trip with no rate ceiling. A compromised NC user session — or a cross-app XSS finding a hole elsewhere in the install — could flood the connected SuiteCRM with thousands of Notes / Tasks / Cases per second, up to the SuiteCRM ACL boundary. Each endpoint now carries #[UserRateThrottle(limit: 30, period: 60)] — 30 writes per minute per user, more than enough for a real human workflow, several orders of magnitude below any abuse curve.
  • A05 Low — avatar Content-Type/cache. getSuiteCRMAvatar() is #[NoCSRFRequired] (so it can be embedded in an <img> src) and cached for 24 hours. The DataDisplayResponse default Content-Type of application/octet-stream combined with a compromised or misconfigured SuiteCRM upstream returning an HTML/SVG blob would let an attacker land a cached blob at the Nextcloud origin. Fix magic-byte-sniffs the payload against a whitelist of JPEG/PNG/GIF/WebP (SVG is deliberately excluded because it can carry <script>) and pins the response Content-Type to the sniffed mime; anything that does not sniff as a real image is served as an empty image/png body. nosniff is already set globally by Nextcloud; this closes the hole for browsers that ignore it.
  • A01 Low — setAdminConfig() accepted arbitrary keys. Mirrored the existing USER_ALLOWED_KEYS pattern with an ADMIN_ALLOWED_KEYS whitelist covering the four keys the admin form actually writes (oauth_instance_url, client_id, client_secret, oauth_authorize_path). Silently drops unknown keys, same permissive-drop model as the user-side endpoint.
  • A09 Low — OAuth error log leaked raw guzzle message. The SuiteCRM OAuth exchange failed log line included 'raw' => $result['error'], which on Guzzle client-error paths can embed the request URL and therefore the one-time authorization code= query parameter. The code is already consumed by the time it hits the log, but log hygiene principle is to redact. Removed the field; the structured http_status / error_code / error_description / error_kind context already carries the actionable diagnostic.
  • Non-OWASP hygiene — URL-encoding consistency. SuiteCRMAPIService.php line 110 built the reminder notification click-through as a raw concat of $suitecrmUrl . '/index.php?module=' . $module . '&action=DetailView&record=' . $elemId. Two sibling call sites (SuiteCRMWidget::buildEventLink() and SuiteCRMCalendarWidget::buildEventLink()) already rawurlencode both parts; the inconsistency was a maintenance trap even though the values are TLS-fetched from the admin-configured SuiteCRM. Third call site now matches.

Widget UX overhaul — 3-dot per-widget settings

Aligns the SuiteCRM widget UX with the sibling integration_forgejo_gitea app: every widget now carries a 3-dot toolbar menu with a Refresh action and a Widget settings modal. Per-user refresh cadence, records-to-show count, and (for eight of the nine widgets) "Only records assigned to me" toggle are stored per widget so users can tune each widget independently.

Settings modal exposes three (four for Calendar / Pipeline) sections per widget: - Refresh frequency — Never / 30s / 1m / 5m / 15m / 30m / 1h (default 5 minutes) - Records to show — 5 / 10 / 15 / 20 / 25 / 50 (default 20). Doubles as the fetch limit sent to the SuiteCRM API so we don't ask for rows we'd drop client-side; server clamps to max(1, min(100, N)) as defence against a tampered client. - Show → Only records assigned to me — checkbox on eight of nine widgets. Defaults ON for Calendar / Cases / Tasks / Pipeline (matches the existing single-user behavior; toggle OFF widens to team-visible records within your SuiteCRM ACL). Defaults OFF for Activities / Contacts / Accounts / Leads (they've always shown any recently-added record; toggle ON filters to your own). Events (reminders) is per-user by SuiteCRM design and has no toggle. - Widget-specific extras — Calendar carries "Include Tasks alongside Meetings and Calls"; Pipeline carries the "Closing this quarter / Top value / Weighted value" framing radio (moved out of PersonalSettings.vue's global block).

  • Widget titles are now SuiteCRM: <Module> (colon-separated), matching the Forgejo widget naming convention. All nine widgets: SuiteCRM: Events, SuiteCRM: Calendar, SuiteCRM: Tasks, SuiteCRM: Cases, SuiteCRM: Pipeline, SuiteCRM: Activities, SuiteCRM: Contacts, SuiteCRM: Accounts, SuiteCRM: Leads.
  • Shared SuiteCRMWidgetShell.vue component replaces the per-widget NcDashboardWidget wrapping. The shell renders the toolbar, the loading / not-connected / error / empty states, the slot for the widget's item list, and the settings modal. The Forgejo sibling inlines this shape per widget; we chose to abstract it here because SuiteCRM has nine widgets that share the same shell — duplicating the toolbar and modal nine times would have been 200+ lines of copy-paste with no functional payoff. The visible UX matches Forgejo exactly: same NcActions layout, same modal structure, same RefreshIntervalPicker behaviour.
  • Ported useAutoRefresh.js composable and RefreshIntervalPicker.vue component verbatim from the Forgejo app (with the app id string swapped) so both apps share the same polling semantics — timer plus wake-signal refetches on tab focus, visibility change, and bfcache restore. setIntervalMs(0) disables the timer, wake-signal refetches still run. Cadence options: never / 30s / 60s / 5min / 15min / 30min / 1h.
  • New per-user pref keys (whitelisted in ConfigController::USER_ALLOWED_KEYS): events_refresh_seconds, calendar_refresh_seconds, tasks_refresh_seconds, cases_refresh_seconds, pipeline_refresh_seconds, activities_refresh_seconds, contacts_refresh_seconds, accounts_refresh_seconds, leads_refresh_seconds. All default to 300s.
  • New GET /widget-config endpoint returns the current value of every widget refresh key plus calendar_show_tasks and pipeline_mode in one round-trip; each widget calls it on mount and passes the seed into useAutoRefresh.setIntervalMs(). Save flow reuses the existing PUT /config endpoint, so the AuthZ gate for widget settings is the same USER_ALLOWED_KEYS whitelist the personal-settings page has always used.
  • Per-widget settings moved out of PersonalSettings.vue and into the widget's own 3-dot menu where they belong:
  • Calendar widget: calendar_show_tasks toggle (include SuiteCRM Tasks alongside Meetings/Calls)
  • Pipeline widget: pipeline_mode radio selector (closing this quarter / top value / weighted) The keys themselves are unchanged, so a 3.0.x install upgrading to 3.1.0 keeps whatever value the user had picked.
  • Shared .scw-* CSS classes (SuiteCRM Widget prefix) added to css/dashboard.css for the item list styling — one place instead of nine scoped <style> blocks with the same declarations. css/dashboard.css was already loaded on every widget page via Util::addStyle() in each widget's load() hook, so no additional PHP registration was needed.
  • Semantic per-widget row icons (via vue-material-design-icons): AccountOutline for Contacts, Handshake for Leads, OfficeBuilding for Accounts, Briefcase for Cases, FormatListChecks for Tasks, TrendingUp for Pipeline. Activities / Calendar / Events dispatch per row type (CalendarClock for Meetings, PhoneOutline for Calls, FormatListChecks for Tasks, Note for Notes, BellRing fallback). Rendered inside a small circular container tinted with --color-primary-element so icons flow with the user's Nextcloud accent. Considered SuiteCRM's own SuitePicons font pack for brand consistency but rejected it (icon font, ~100KB with no CDN, glyph-map not published) — MDI is already installed and gives the same semantic clarity at ~200 bytes per icon.
  • CheckCircleOutline empty-state icon on every widget so a widget with zero results still reads as "you're all caught up" rather than a blank card.
  • List behaviour: capped at max-height: 320px with overflow-y: auto. When the user's picked records count fits, the widget is naturally that height; when it doesn't, the list scrolls internally with a subtle scrollbar and the trailing "Show all →" link stays anchored below.
  • Widget titles renamed to SuiteCRM: <Module> (colon-separated) matching Forgejo's naming convention — SuiteCRM: Cases, SuiteCRM: Pipeline, etc.
  • Dropped IAPIWidget / IAPIWidgetV2 from all nine widget classes so the Vue shell renders instead of NC's server-side WidgetItem card. The Vue shell is what carries the 3-dot toolbar and settings modal; NC's own WidgetItem card doesn't have a slot for those. The getItems* / getItemsV2* methods are kept as inert dead code so re-declaring the interface is a one-line change if a future NC version's dashboard app changes its precedence logic.
  • Built js/ bundles are now committed to git so a git checkout v<tag> on a deploy host lands a runnable app without needing node/npm on the host. Matches integration_forgejo_gitea's convention. Regenerate with npm run build before any release commit.

Notes for maintainers

  • Nextcloud release cadence: whenever a new Nextcloud major ships, bump <nextcloud max-version> in appinfo/info.xml, cross-check the PHP min-version against the new server's system-requirements page, and mirror any PHP floor change into composer.json (require and config.platform). A mismatch between the two is silent until someone tries to build on a workstation with an older PHP.
  • Verify the version-bearing files stay in lock-step before commit: appinfo/info.xml, package.json, package-lock.json, CHANGELOG.md, and the git tag. The npm run verify:version hook covers package.json vs appinfo/info.xml; the rest is manual review at commit time.
  • Icon file convention: img/app.svg + img/app-dark.svg are for the Nextcloud app-menu (picked up by NC by filename, mono contract). img/app-color.svg is the brand mark used inside the app's own widgets and settings pages. Do not repoint img/app.svg to the color asset — the app-menu contract expects mono ink that flows with the theme.
  • OWASP review baseline: 3.1.0 clears 0 High + 0 Medium findings on a post-iteration re-review. Every previously-fixed Medium/Low from the first pass is still applied. Four remaining Low advisories are documented tradeoffs (SuiteCRM ACL delegation on onlyMine=false, sensitive column NC-30 whitelist, committed js/ supply-chain note, reference-provider fan-out on paste). Any future write endpoint added to SuiteCRMAPIController needs its own #[UserRateLimit] attribute or an explicit reason in the class doc why it does not. Any future admin-writable config key needs a line in ADMIN_ALLOWED_KEYS and, if it holds a secret, an entry in the migration's SENSITIVE_APPCONFIG_KEYS. Any future widget-list endpoint needs $limit = $this->clampLimit($limit) before hitting the service.
  • New per-widget pref keys must be added to USER_ALLOWED_KEYS in ConfigController AND to the getWidgetConfig() return object AND to the widget's own Vue loadWidgetConfig() reader. Missing any one of the three silently drops the value from the read path or the save path.
  • Widget shell contract: SuiteCRMWidgetShell.vue emits refresh (user hit the refresh action) and save({ refreshSeconds, onlyMine, maxItems, extras, close }) (user hit Save in the modal). Callers must invoke close() inside their save flow — the shell does not auto-close so the caller can leave the modal open if the save failed and it wants the user to retry without re-typing.
  • Regression call-out: the previous "Follow-up Task from a calendar item" action (README line 55) is not currently reachable in the dashboard UI after the Vue widget rewrite; the modal component TaskFollowupModal.vue and the PHP createFollowupTask endpoint are still live but not wired. Re-adding this as a per-row action in the shell is a 3.2.0 scope item.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignaturepXX3njPlrc5StGaxF2bb6qjBDTWnAuMA/SV2M86Wv2linGxlX0aXL8plrixqrBIq
EQwOIaYFeXQM3k7kiIoYQhYcndlKXFRKoN6v1vo63zao/Zi2xlM6V7UoRGzsmBv7
ACiQo70Ia2ETG8ZF8dVUUlwFZ5Pq5ipccgjQe6EgOVu9fwmJQ+NDdhK4LUo4STRZ
QGsuApeNo7G2QyhsPfj+BkgZ1saicpqT8i0eG7OUBPfiyaB3YvkqV2nl6U3ocDcZ
NgbtkRPQCaIa2JymEzQTchZX/HbEqYrPJgaPd/VvMXTrd59fMz0serAevOZROdNJ
zGz+cx846ow83NdyOK931XIgINi6RWY9f2vtfdJCaxKen4mxtOHDXg0PmII1qJCl
gKDgMu3bOKs3RwtSeqPG3TOSN49PCmB4Wj6nM6QihA/dTpgHOSQ2JmpxKE/Zae/j
8Hmh23juWe4diixVnK4+oxzqfSBS4bqHTMtEsS6L5VDpnBYc75CK2w6vafj4xZ+/
CwlZbhX9T7tVYT9pqrazuBn1FzTm3xeHonKxRGt3OzfIPkM/ehA15ESGvldV57NG
G34UMXHhiijPT6fH0U5cARByHDcwWZSam1Iv+SwfZ8ysKZtQzb2NDrDwEpIfchZu
83qX8vpXhVD3KqFj4PS+MwI/rQ66Fh0hHoD+rBpheNA=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.0.2
Release Details
UpdatedJuly 29, 2026, 2:54 p.m.
Changelog

App Store metadata refresh, take two. The 3.0.1 upload was rejected by the App Store validator with Element 'summary': [facet 'maxLength'] The value has a length of '166'; this exceeds the allowed maximum length of '128'., so this release trims <summary> under the 128-character cap while keeping the widget-count and coverage cue that made the new listing useful in the first place.

Changed

  • appinfo/info.xml <summary> shortened from 166 to 117 characters to satisfy the App Store's apps/info.xsd maxLength facet on <summary>. New form drops the module enumeration and keeps the "nine widgets" specificity: "Nine dashboard widgets, unified search and notifications for SuiteCRM 8.x. Calendar, tasks, cases, pipeline and more." The full nine-widget breakdown is still in <description>, which the schema does not cap.

Notes for maintainers

The apps/info.xsd caps to remember when editing the top of appinfo/info.xml: - <name> max 40 characters - <summary> max 128 characters - <description> no practical cap (Markdown-friendly, use it for the full pitch)

Add a wc -c check on the summary line to release-nc-app.sh phase 1 to catch this locally next time.

Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignatureX4/pzvjKgONWyCtn4SRPhCb11mYQF8NyrEtydiOD9dttFgiLxLwcYhEQJfdBKLxY
KiG0c53jkSZHI0osL7UoSRWBu61KYv5AoRHS9ajdJSXD7xXR46nl56LH//teun3+
tbLSa2YsyNbIlcXDy8/ZeMrNApHk0HkEi9N8yv/m+sPERK975Oy8CrmgfGk7afuW
AkCY1NuDvZL4I5Dstt30Z6BW94hyiYXtky2W+voCjB2nAd6P/k5gGShDw61BrZnf
q1Y3moshtF/XV5niYLsvsj55mL24E8x22/VeMCYzQMKQtAahO8dcl9YrZ5rqho/b
NrxOFNrRVboUIZu5LSkHJgGC8y4GFwKUeyWtzKK6qUESaGU7qZlgi78ISdrh09yP
RLCmZBdE0QLOK0ZZ+Y9wWrq4/3+eE+5QFGJ3Itc4kvvbw80NIOhFL74NzAH15CuP
AFZ6EHUKNpqZ/9Ek0ppmRAaYrPnBsVqSCvDnRFVL8a3mi6Beul1B2Tixpx0wOKzd
wy5nx/eXEwBXBZJY91CgQDUFNMKMflxlIXgUIpAWUAjEemKlUJojxcmbB3+oNUPz
t8XTqPkcKedrDnLBdmyai6G2CTT187b6gx3u+qX2GT0HY1NG1x9PpH6+0ubL3loP
5GyLf0FkVjBVMgCVx08wRCnYWkTokDobVeDrHDkFiDo=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<35.0.0
Minimum Integer bits32
PHP>=8.2.0
SuiteCRM integration 3.0.0
Release Details
UpdatedJuly 29, 2026, 1:38 p.m.
Changelog

Renames the Nextcloud app id from njordium_suitecrm back to integration_suitecrm. This is the only breaking change in the release. Every setting on your existing 2.x install, admin OAuth config (instance URL, client ID/secret, authorize path), every per-user OAuth token, every widget preference (pipeline_mode, quick_actions_enabled, calendar_show_tasks) carries across automatically via a Repair step that runs on occ upgrade. Users do not need to re-authorise SuiteCRM.

Why the rename. When the fork first shipped in 2.0.0, Julien Veyssier's original integration_suitecrm App Store record was stale and blocked our updates from reaching his ~200 existing installs. The pragmatic response was to rename the fork's app id to njordium_suitecrm so we could ship on the App Store under our own record. In July 2026, Julien transferred ownership of the original record to this fork (see nextcloud/app-certificate-requests#1104 and #1114), which removed the original blocker. 3.0.0 restores the canonical integration_suitecrm app id so:

  • Julien's existing 1.x installs get updates seamlessly from the App Store, no manual reinstall.
  • There's one canonical id on the store, no user confusion about which of two records to install.
  • The GitHub org stays njordium/integration_suitecrm — the App Store app id and the GitHub org name are decoupled.

See docs/upgrade-2.x-to-3.0.md for the end-to-end upgrade walkthrough, rollback procedure, and post-upgrade verification steps.

Changed

  • App id renamed from njordium_suitecrm back to integration_suitecrm. The install folder is now custom_apps/integration_suitecrm/; the old custom_apps/njordium_suitecrm/ will no longer be recognised. Every HTTP route the frontend calls (/apps/integration_suitecrm/*) and every internal string reference has been updated in a single atomic pass across 35 files.
  • occ command name is now occ integration_suitecrm:test-connection. The 2.x form occ njordium_suitecrm:test-connection no longer resolves.
  • OAuth redirect URL on the SuiteCRM OAuth2 client must be updated from <nextcloud>/apps/njordium_suitecrm/oauth-callback to <nextcloud>/apps/integration_suitecrm/oauth-callback. Byte-for-byte match required. Same manual step users hit on the 1.9 to 2.0 upgrade; instructions in docs/upgrade-2.x-to-3.0.md.
  • Webpack bundle filenames flipped from njordium_suitecrm-*.js back to integration_suitecrm-*.js. Direct-install admins with cached JS in a reverse proxy should invalidate the cache on upgrade.
  • Migration\CopyLegacyAppConfig SQL body unchanged, LEGACY_APP_ID constant flipped from integration_suitecrm (2.0.0) to njordium_suitecrm (3.0.0). The class continues to run on every occ upgrade as a post-migration Repair step. On a fresh install (no rows to copy) it's a silent no-op.

Migration path

  • From 2.x (any 2.0-2.6 release) to 3.0.0: occ upgrade runs Migration\CopyLegacyAppConfig which copies every oc_appconfig row where appid='njordium_suitecrm' and every oc_preferences row for the same app id into appid='integration_suitecrm'. Legacy rows are left in place so rollback stays trivial. Encrypted OAuth tokens survive the copy because they live in oc_preferences under app id-scoped rows.
  • From 1.9.x directly to 3.0.0: oc_appconfig and oc_preferences rows are already under integration_suitecrm (that was Julien's app id), so the Repair step is a silent no-op and the deployment picks the settings up under the same id automatically.

Rollback

If 3.0.0 misbehaves, rollback to 2.6.0 is safe because the migration is copy-only, not move: occ app:disable integration_suitecrm && occ app:enable njordium_suitecrm && occ upgrade restores every setting. Detailed steps in docs/upgrade-2.x-to-3.0.md.

Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
Signatureogxzom4kGvfPpSAHfscVLCco55DbReJH1/78dRCmY1oXJ2XiHb1LEXA33NKJMSEt
OBKUO3MKw31GLqvjOSh/q1nIThsgvMviEnvqSHKf3A3Xd9zeJCW0gtuQsFrFGrIV
qsw16PpfY8qwa/WSpV7i3hVEY38BohCGP+GtlTiTg4OZUouyBkt7c5xqO6PlhXBX
fkIJepB1Lf6NLFa9B67Lxuzn0LLXG9BHamrfhCMvENcoJ3gfEXlbqB8/Isxg7i9D
hzgQD+pR/l6nNgJVwT8oatJpeS4SiarOH2RTMxSIkbBU1d9GgXTRSEZ2zLmU1/do
R2W9vcNWnLfNYpIzWVy9UQNG/OLd7aGGMp6Q5EJSKPQGleKLlCzoVwyb7ghNu6fU
NDeEpJQu9wNmCTEWJKzXrDiENJKAAoWHocLv5CL00xK/yGRTXj3of6NOmUfDkAq5
TfHMQcxnpj7PG103qqaZI/JQ3X49MXDbe/BOnXafNyhEXJVReYHTqXfSk+HTE4dm
VwJjN8cjunijsld5PLDWY3xyK8s/or+5xgikAwm5apcI/Masfk/FGsPWq4NJUXQN
VsQOX5KwNrGdPZmxxMdd5Rov8CkC9eE/Yrtwhem8fd1vs8b0T88PPv/224Fwb4jJ
oI9709FIemcZF3xrQTMPnwOmfKIVKiGVlOsva3IbYl8=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<35.0.0
Minimum Integer bits32
PHP>=8.2.0

Nextcloud 30

SuiteCRM integration 3.2.4
Release Details
UpdatedAug. 23, 2026, 8:33 a.m.
Changelog

Deprecation cleanup.

  • Notifier::prepare() now throws \OCP\Notification\UnknownNotificationException instead of \InvalidArgumentException when a notification isn't ours (or is a reminder for an unknown SuiteCRM module). Since Nextcloud 30, \InvalidArgumentException is the deprecated contract and every /ocs/v2.php/apps/notifications poll emitted a level-2 warning naming this notifier; the new class is the NC-30+ replacement. Behaviour is identical (row still suppressed on unknown subject); the warning line stops.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
Signatureg0H49K2pxy0oLASZymk71eZc/SqJV/l4y3hxSShrXRSFql30MsfhwCsKajn1AhPWcbu2y/hnFHf/SToF3IqVyiRLitcd80FgetIzgfyMGrzIyvzvNCngZKf6IHATuMu45deRGUp760yX51KsL9EGFDlbtiZpc1OeazbDpWfpBvqISRXhd0KEo6jhpLZCT9N3kPUOo4DbGzrq6PA3JbJA0zUuYXKoFAMGKvxyhm2TgYP7Q+ET7pkYpV4EVytSbNwBcfV5PPjAG55MdHM2Mg4Y22ppJraR2s7vGec3wl/iX6Oyf2jsjVj4F6In4wI6A+QWllyD4rVzvQ6YOvwTtaHJYckoKcTIs7RpfJiAgjHkzYnVVXnPdXLlBCt7rlCqxSBkqhkQngdobjAAuyYSr1bV+JySSXAH2n6vskEzSC+FNkz5TgzAqMcc8I0VOVpGpDhSpgUb4bbbX8gviMEEWQQu1tUuYFFUVqQcy9Zpt1jF/HcOBEL1k5sg1hHsk6r1DqB75fPqGOugxiNsEMa73iYiaV0HDUrMOybGtLkzDxTxRShJk/zN6D03oUSBAE1gaebu4mjvlz/KWvyh++nJH6AnIhiUbxgPfwTGXFN64qQ1zeKSuSUXrijLjaZ0BE2RQtXUvPjgJlq5V+txa/rNCL5ThHkn0yMiLcqhJB4QEq3P0Ig=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.2.3
Release Details
UpdatedAug. 15, 2026, 4:27 p.m.
Changelog

Admin-panel copy polish.

  • "OAuth authorize endpoint path" label shortened to "Authorize path" so it lines up visually with the other short-form labels ("Instance address", "OAuth client ID / secret", "Redirect URI") in the harmonised grid layout.
  • The trailing helper text ("SuiteCRM 8.10.x default: /Api/authorize. Older installs may use /legacy/oauth2/authorize.") is removed. The field still pre-fills with /Api/authorize on a fresh install via Admin.php's getValueString(..., '/Api/authorize') default, so an admin who never had to touch this row still sees the correct value.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignatureD/6aAuPWTF4VDuLA6GAOcjkW5AQK964/Pp8pVssyhws6pCt4Lpm4KHiN8djxiWA/VRCvLGtN70XmSYkJHBMZi1c6f4J2hawz/QmI+5GV03Y28c3oEt7rVSzNxOk7Q6mugRvyodjc0ar+eoiMPOXLuQoMQhjlUpNyIwILBL5hRVftviknUoP2FwbQeKIecc6biii7QqYtiOy+/s8o1PQRBQSE9/UiHvVEQeKHOWGVWX6ny3bHkzvEASQcWMStsfPR7eaEGZP8zOOeCrchinzdztxxogh0i0NCb/5D/4wao0gn8KNfTo7rtI1/TzjxN/eqgLHm2vd9uXYNk+cT0R51wqESAJrS2xPdYLsBWi6MJUyjXTLTP3ZAhvhYrO3XUqTL6QLDSt0wFGaqXx+xw7OYU3ryl8Ox3hMnhOKs5VuiMc9KSEcGPJ+GY18RyIJFvOLV02rYPdmboeSCiD2/n4beIrdMmfhiyErDmAaO0lo2hoA371UP6ghSorgH/aax4LdSYzJ37Ua8Ssm+nvq2NK9KqZXcPoupmBlAojq3o22oVfQcYr49p57af3pFBd6/MPl2 5sI5is0nhBuVMo6jLghs9ShMWMg2QAYsrTxLNoPiRmfqDNCPl0Z+x+9Rg5dgq2g2YRaEC1cfQEi4p0938FrE+LI02zlBeLokZ8l0/fTysRA=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.1.0
Release Details
UpdatedAug. 14, 2026, 7:50 p.m.
Changelog

Three cross-cutting changes go out in one tag: the Nextcloud 35 compatibility bump, color-brand widget icons across every dashboard surface, and the fixes surfaced by a full OWASP-Top-10 secure-coding review. Nothing changes for the end user's day-to-day interaction with the widgets, but the App Store now offers the update to Nextcloud 35 installs, admins see the SuiteCRM brand mark instead of a mono glyph on the dashboard and settings pages, and the security posture picks up rate limits on the four write endpoints, mime-pinning on the avatar proxy, an admin-config whitelist, plaintext-leak fixes on the OAuth log, and a hardening pass on the 2.x → 3.x config migration so client_secret no longer downgrades to admin-inspectable plaintext on upgrade.

Existing installs on 30–34 continue to work unchanged. Admins on PHP 8.2 need to upgrade PHP before installing this release.

Nextcloud 35 compatibility

  • appinfo/info.xml <nextcloud max-version> bumped from 34 to 35 so the App Store lists this release as compatible with the Nextcloud 35 line. Minimum stays at 30 — the widget code, dashboard API, notification and search integrations all use surface that has been stable across 30–35, so there is no reason to raise the floor.
  • appinfo/info.xml <php min-version> bumped from 8.2 to 8.3 to match Nextcloud 35's own PHP baseline. Nextcloud 35 will not boot on PHP 8.2, so advertising 8.2 support would let admins install the app onto a host that cannot actually run the server hosting it.
  • composer.json PHP constraint lifted to ^8.3 in both require and the config.platform block so composer install resolves against the same platform Nextcloud 35 runs on. The config.platform pin matters for anyone building the release tarball on a workstation that still has PHP 8.2 available — without it, composer would happily select older dependency versions than the ones the server actually loads.
  • appinfo/info.xml <author> order reordered so Kim Haverblad (current maintainer, homepage="https://njordium.com") appears above Julien Veyssier (original 1.x author). The App Store renders <author> elements in document order in the sidebar of apps.nextcloud.com/apps/integration_suitecrm; leading with the maintainer who ships the releases matches what users see in the App Store listing for the release they are actually installing. Julien's contribution is still credited — this is an ordering change, not a removal.

Color widget brand icon

Mirrors the pattern shipped in the sibling integration_forgejo_gitea app, which uses a single brand-color SVG across every dashboard widget header and the admin/personal settings anchor. The img/app-color.svg asset was already in the repository from earlier work but not wired anywhere.

  • css/dashboard.css .icon-suitecrm collapsed from the two-rule mono theme-swap (app-dark.svg on light theme, app.svg on dark theme) to a single rule pointing at img/app-color.svg. The color logo reads well against both themes, so no body.theme--dark override is needed. All nine dashboard widget classes (SuiteCRMWidget, SuiteCRMCalendarWidget, SuiteCRMTasksWidget, SuiteCRMCasesWidget, SuiteCRMPipelineWidget, SuiteCRMActivitiesWidget, SuiteCRMContactsWidget, SuiteCRMAccountsWidget, SuiteCRMLeadsWidget) pick this up through their existing getIconClass(): return 'icon-suitecrm' return, so no per-widget PHP change was needed for the CSS layer.
  • Each widget's getIconUrl() now returns the color asset instead of the mono app.svg. The Nextcloud 30+ dashboard app prefers getIconUrl() over getIconClass() because it renders the icon outside the app's own stylesheet context (mail digests, dashboard permalinks); the two paths now agree.
  • SuiteCRMWidget::iconForModule() and SuiteCRMCalendarWidget::iconForModule() per-item icon fallback (the icon shown next to a row when the module is not Calls/Meetings) is now app-color.svg so a Task or Note row carries the brand mark rather than the mono glyph.
  • Vue settings anchorssrc/components/PersonalSettings.vue and src/components/AdminSettings.vue inline the same collapsed rule so the little icon next to the "Connected accounts" / admin settings heading matches the dashboard widgets.
  • Vue view avatar fallback — the seven views that hand SuiteCRM rows to NcListItem (Activities.vue, Cases.vue, Tasks.vue, Pipeline.vue, RecentContacts.vue, RecentAccounts.vue, RecentLeads.vue) now fall back to app-color.svg instead of app.svg when a row has no per-record avatar, so the visual grammar is consistent across the whole app surface.
  • App-menu icons (img/app.svg, img/app-dark.svg) are unchanged. Nextcloud picks those up by file convention for the top navigation bar, where the mono contract still applies — same split the Forgejo/Gitea app uses.

Secure-coding review fixes (OWASP Top-10 pass)

Ran a full static analysis over lib/, src/, templates/, and appinfo/ against the OWASP Top-10 (2021) taxonomy. Zero High findings. Three Medium and four Low findings are fixed in this release; the JWT-signature note (A02) is documented as accepted-risk in the source and needs no code change.

  • A02 Medium — Migration\CopyLegacyAppConfig copied oc_appconfig rows via a raw QueryBuilder INSERT of (appid, configkey, configvalue), which left the sensitive column at its table default of false. Any 2.x install upgrading to 3.x therefore had its client_secret land under the new app id as admin-inspectable plaintext — visible in occ config:app:get integration_suitecrm client_secret and in Nextcloud support-bundle dumps. Fix routes the write through IAppConfig::setValueString with an explicit sensitive: argument, derived from a new SENSITIVE_APPCONFIG_KEYS class-const whitelist. A structural regression test (CopyLegacyAppConfigTest::testAppConfigCopyPreservesSensitiveFlag) locks the guarantee. The migration only selects configkey + configvalue; the sensitive column was added to oc_appconfig in Nextcloud 31, and selecting it on Nextcloud 30 aborts the entire repair step with SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sensitive'. The whitelist alone gives the same practical protection because client_secret is the only sensitive key this app persists.
  • A04 Medium — write endpoints unbounded. The four #[NoAdminRequired] POST endpoints createFollowupTask, logNote, linkDeckCard, and emailToCase all previously accepted requests one-per-round-trip with no rate ceiling. A compromised NC user session — or a cross-app XSS finding a hole elsewhere in the install — could flood the connected SuiteCRM with thousands of Notes / Tasks / Cases per second, up to the SuiteCRM ACL boundary. Each endpoint now carries #[UserRateThrottle(limit: 30, period: 60)] — 30 writes per minute per user, more than enough for a real human workflow, several orders of magnitude below any abuse curve.
  • A05 Low — avatar Content-Type/cache. getSuiteCRMAvatar() is #[NoCSRFRequired] (so it can be embedded in an <img> src) and cached for 24 hours. The DataDisplayResponse default Content-Type of application/octet-stream combined with a compromised or misconfigured SuiteCRM upstream returning an HTML/SVG blob would let an attacker land a cached blob at the Nextcloud origin. Fix magic-byte-sniffs the payload against a whitelist of JPEG/PNG/GIF/WebP (SVG is deliberately excluded because it can carry <script>) and pins the response Content-Type to the sniffed mime; anything that does not sniff as a real image is served as an empty image/png body. nosniff is already set globally by Nextcloud; this closes the hole for browsers that ignore it.
  • A01 Low — setAdminConfig() accepted arbitrary keys. Mirrored the existing USER_ALLOWED_KEYS pattern with an ADMIN_ALLOWED_KEYS whitelist covering the four keys the admin form actually writes (oauth_instance_url, client_id, client_secret, oauth_authorize_path). Silently drops unknown keys, same permissive-drop model as the user-side endpoint.
  • A09 Low — OAuth error log leaked raw guzzle message. The SuiteCRM OAuth exchange failed log line included 'raw' => $result['error'], which on Guzzle client-error paths can embed the request URL and therefore the one-time authorization code= query parameter. The code is already consumed by the time it hits the log, but log hygiene principle is to redact. Removed the field; the structured http_status / error_code / error_description / error_kind context already carries the actionable diagnostic.
  • Non-OWASP hygiene — URL-encoding consistency. SuiteCRMAPIService.php line 110 built the reminder notification click-through as a raw concat of $suitecrmUrl . '/index.php?module=' . $module . '&action=DetailView&record=' . $elemId. Two sibling call sites (SuiteCRMWidget::buildEventLink() and SuiteCRMCalendarWidget::buildEventLink()) already rawurlencode both parts; the inconsistency was a maintenance trap even though the values are TLS-fetched from the admin-configured SuiteCRM. Third call site now matches.

Widget UX overhaul — 3-dot per-widget settings

Aligns the SuiteCRM widget UX with the sibling integration_forgejo_gitea app: every widget now carries a 3-dot toolbar menu with a Refresh action and a Widget settings modal. Per-user refresh cadence, records-to-show count, and (for eight of the nine widgets) "Only records assigned to me" toggle are stored per widget so users can tune each widget independently.

Settings modal exposes three (four for Calendar / Pipeline) sections per widget: - Refresh frequency — Never / 30s / 1m / 5m / 15m / 30m / 1h (default 5 minutes) - Records to show — 5 / 10 / 15 / 20 / 25 / 50 (default 20). Doubles as the fetch limit sent to the SuiteCRM API so we don't ask for rows we'd drop client-side; server clamps to max(1, min(100, N)) as defence against a tampered client. - Show → Only records assigned to me — checkbox on eight of nine widgets. Defaults ON for Calendar / Cases / Tasks / Pipeline (matches the existing single-user behavior; toggle OFF widens to team-visible records within your SuiteCRM ACL). Defaults OFF for Activities / Contacts / Accounts / Leads (they've always shown any recently-added record; toggle ON filters to your own). Events (reminders) is per-user by SuiteCRM design and has no toggle. - Widget-specific extras — Calendar carries "Include Tasks alongside Meetings and Calls"; Pipeline carries the "Closing this quarter / Top value / Weighted value" framing radio (moved out of PersonalSettings.vue's global block).

  • Widget titles are now SuiteCRM: <Module> (colon-separated), matching the Forgejo widget naming convention. All nine widgets: SuiteCRM: Events, SuiteCRM: Calendar, SuiteCRM: Tasks, SuiteCRM: Cases, SuiteCRM: Pipeline, SuiteCRM: Activities, SuiteCRM: Contacts, SuiteCRM: Accounts, SuiteCRM: Leads.
  • Shared SuiteCRMWidgetShell.vue component replaces the per-widget NcDashboardWidget wrapping. The shell renders the toolbar, the loading / not-connected / error / empty states, the slot for the widget's item list, and the settings modal. The Forgejo sibling inlines this shape per widget; we chose to abstract it here because SuiteCRM has nine widgets that share the same shell — duplicating the toolbar and modal nine times would have been 200+ lines of copy-paste with no functional payoff. The visible UX matches Forgejo exactly: same NcActions layout, same modal structure, same RefreshIntervalPicker behaviour.
  • Ported useAutoRefresh.js composable and RefreshIntervalPicker.vue component verbatim from the Forgejo app (with the app id string swapped) so both apps share the same polling semantics — timer plus wake-signal refetches on tab focus, visibility change, and bfcache restore. setIntervalMs(0) disables the timer, wake-signal refetches still run. Cadence options: never / 30s / 60s / 5min / 15min / 30min / 1h.
  • New per-user pref keys (whitelisted in ConfigController::USER_ALLOWED_KEYS): events_refresh_seconds, calendar_refresh_seconds, tasks_refresh_seconds, cases_refresh_seconds, pipeline_refresh_seconds, activities_refresh_seconds, contacts_refresh_seconds, accounts_refresh_seconds, leads_refresh_seconds. All default to 300s.
  • New GET /widget-config endpoint returns the current value of every widget refresh key plus calendar_show_tasks and pipeline_mode in one round-trip; each widget calls it on mount and passes the seed into useAutoRefresh.setIntervalMs(). Save flow reuses the existing PUT /config endpoint, so the AuthZ gate for widget settings is the same USER_ALLOWED_KEYS whitelist the personal-settings page has always used.
  • Per-widget settings moved out of PersonalSettings.vue and into the widget's own 3-dot menu where they belong:
  • Calendar widget: calendar_show_tasks toggle (include SuiteCRM Tasks alongside Meetings/Calls)
  • Pipeline widget: pipeline_mode radio selector (closing this quarter / top value / weighted) The keys themselves are unchanged, so a 3.0.x install upgrading to 3.1.0 keeps whatever value the user had picked.
  • Shared .scw-* CSS classes (SuiteCRM Widget prefix) added to css/dashboard.css for the item list styling — one place instead of nine scoped <style> blocks with the same declarations. css/dashboard.css was already loaded on every widget page via Util::addStyle() in each widget's load() hook, so no additional PHP registration was needed.
  • Semantic per-widget row icons (via vue-material-design-icons): AccountOutline for Contacts, Handshake for Leads, OfficeBuilding for Accounts, Briefcase for Cases, FormatListChecks for Tasks, TrendingUp for Pipeline. Activities / Calendar / Events dispatch per row type (CalendarClock for Meetings, PhoneOutline for Calls, FormatListChecks for Tasks, Note for Notes, BellRing fallback). Rendered inside a small circular container tinted with --color-primary-element so icons flow with the user's Nextcloud accent. Considered SuiteCRM's own SuitePicons font pack for brand consistency but rejected it (icon font, ~100KB with no CDN, glyph-map not published) — MDI is already installed and gives the same semantic clarity at ~200 bytes per icon.
  • CheckCircleOutline empty-state icon on every widget so a widget with zero results still reads as "you're all caught up" rather than a blank card.
  • List behaviour: capped at max-height: 320px with overflow-y: auto. When the user's picked records count fits, the widget is naturally that height; when it doesn't, the list scrolls internally with a subtle scrollbar and the trailing "Show all →" link stays anchored below.
  • Widget titles renamed to SuiteCRM: <Module> (colon-separated) matching Forgejo's naming convention — SuiteCRM: Cases, SuiteCRM: Pipeline, etc.
  • Dropped IAPIWidget / IAPIWidgetV2 from all nine widget classes so the Vue shell renders instead of NC's server-side WidgetItem card. The Vue shell is what carries the 3-dot toolbar and settings modal; NC's own WidgetItem card doesn't have a slot for those. The getItems* / getItemsV2* methods are kept as inert dead code so re-declaring the interface is a one-line change if a future NC version's dashboard app changes its precedence logic.
  • Built js/ bundles are now committed to git so a git checkout v<tag> on a deploy host lands a runnable app without needing node/npm on the host. Matches integration_forgejo_gitea's convention. Regenerate with npm run build before any release commit.

Notes for maintainers

  • Nextcloud release cadence: whenever a new Nextcloud major ships, bump <nextcloud max-version> in appinfo/info.xml, cross-check the PHP min-version against the new server's system-requirements page, and mirror any PHP floor change into composer.json (require and config.platform). A mismatch between the two is silent until someone tries to build on a workstation with an older PHP.
  • Verify the version-bearing files stay in lock-step before commit: appinfo/info.xml, package.json, package-lock.json, CHANGELOG.md, and the git tag. The npm run verify:version hook covers package.json vs appinfo/info.xml; the rest is manual review at commit time.
  • Icon file convention: img/app.svg + img/app-dark.svg are for the Nextcloud app-menu (picked up by NC by filename, mono contract). img/app-color.svg is the brand mark used inside the app's own widgets and settings pages. Do not repoint img/app.svg to the color asset — the app-menu contract expects mono ink that flows with the theme.
  • OWASP review baseline: 3.1.0 clears 0 High + 0 Medium findings on a post-iteration re-review. Every previously-fixed Medium/Low from the first pass is still applied. Four remaining Low advisories are documented tradeoffs (SuiteCRM ACL delegation on onlyMine=false, sensitive column NC-30 whitelist, committed js/ supply-chain note, reference-provider fan-out on paste). Any future write endpoint added to SuiteCRMAPIController needs its own #[UserRateLimit] attribute or an explicit reason in the class doc why it does not. Any future admin-writable config key needs a line in ADMIN_ALLOWED_KEYS and, if it holds a secret, an entry in the migration's SENSITIVE_APPCONFIG_KEYS. Any future widget-list endpoint needs $limit = $this->clampLimit($limit) before hitting the service.
  • New per-widget pref keys must be added to USER_ALLOWED_KEYS in ConfigController AND to the getWidgetConfig() return object AND to the widget's own Vue loadWidgetConfig() reader. Missing any one of the three silently drops the value from the read path or the save path.
  • Widget shell contract: SuiteCRMWidgetShell.vue emits refresh (user hit the refresh action) and save({ refreshSeconds, onlyMine, maxItems, extras, close }) (user hit Save in the modal). Callers must invoke close() inside their save flow — the shell does not auto-close so the caller can leave the modal open if the save failed and it wants the user to retry without re-typing.
  • Regression call-out: the previous "Follow-up Task from a calendar item" action (README line 55) is not currently reachable in the dashboard UI after the Vue widget rewrite; the modal component TaskFollowupModal.vue and the PHP createFollowupTask endpoint are still live but not wired. Re-adding this as a per-row action in the shell is a 3.2.0 scope item.
Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignaturepXX3njPlrc5StGaxF2bb6qjBDTWnAuMA/SV2M86Wv2linGxlX0aXL8plrixqrBIq
EQwOIaYFeXQM3k7kiIoYQhYcndlKXFRKoN6v1vo63zao/Zi2xlM6V7UoRGzsmBv7
ACiQo70Ia2ETG8ZF8dVUUlwFZ5Pq5ipccgjQe6EgOVu9fwmJQ+NDdhK4LUo4STRZ
QGsuApeNo7G2QyhsPfj+BkgZ1saicpqT8i0eG7OUBPfiyaB3YvkqV2nl6U3ocDcZ
NgbtkRPQCaIa2JymEzQTchZX/HbEqYrPJgaPd/VvMXTrd59fMz0serAevOZROdNJ
zGz+cx846ow83NdyOK931XIgINi6RWY9f2vtfdJCaxKen4mxtOHDXg0PmII1qJCl
gKDgMu3bOKs3RwtSeqPG3TOSN49PCmB4Wj6nM6QihA/dTpgHOSQ2JmpxKE/Zae/j
8Hmh23juWe4diixVnK4+oxzqfSBS4bqHTMtEsS6L5VDpnBYc75CK2w6vafj4xZ+/
CwlZbhX9T7tVYT9pqrazuBn1FzTm3xeHonKxRGt3OzfIPkM/ehA15ESGvldV57NG
G34UMXHhiijPT6fH0U5cARByHDcwWZSam1Iv+SwfZ8ysKZtQzb2NDrDwEpIfchZu
83qX8vpXhVD3KqFj4PS+MwI/rQ66Fh0hHoD+rBpheNA=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<36.0.0
Minimum Integer bits32
PHP>=8.3.0
SuiteCRM integration 3.0.2
Release Details
UpdatedJuly 29, 2026, 2:54 p.m.
Changelog

App Store metadata refresh, take two. The 3.0.1 upload was rejected by the App Store validator with Element 'summary': [facet 'maxLength'] The value has a length of '166'; this exceeds the allowed maximum length of '128'., so this release trims <summary> under the 128-character cap while keeping the widget-count and coverage cue that made the new listing useful in the first place.

Changed

  • appinfo/info.xml <summary> shortened from 166 to 117 characters to satisfy the App Store's apps/info.xsd maxLength facet on <summary>. New form drops the module enumeration and keeps the "nine widgets" specificity: "Nine dashboard widgets, unified search and notifications for SuiteCRM 8.x. Calendar, tasks, cases, pipeline and more." The full nine-widget breakdown is still in <description>, which the schema does not cap.

Notes for maintainers

The apps/info.xsd caps to remember when editing the top of appinfo/info.xml: - <name> max 40 characters - <summary> max 128 characters - <description> no practical cap (Markdown-friendly, use it for the full pitch)

Add a wc -c check on the summary line to release-nc-app.sh phase 1 to catch this locally next time.

Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
SignatureX4/pzvjKgONWyCtn4SRPhCb11mYQF8NyrEtydiOD9dttFgiLxLwcYhEQJfdBKLxY
KiG0c53jkSZHI0osL7UoSRWBu61KYv5AoRHS9ajdJSXD7xXR46nl56LH//teun3+
tbLSa2YsyNbIlcXDy8/ZeMrNApHk0HkEi9N8yv/m+sPERK975Oy8CrmgfGk7afuW
AkCY1NuDvZL4I5Dstt30Z6BW94hyiYXtky2W+voCjB2nAd6P/k5gGShDw61BrZnf
q1Y3moshtF/XV5niYLsvsj55mL24E8x22/VeMCYzQMKQtAahO8dcl9YrZ5rqho/b
NrxOFNrRVboUIZu5LSkHJgGC8y4GFwKUeyWtzKK6qUESaGU7qZlgi78ISdrh09yP
RLCmZBdE0QLOK0ZZ+Y9wWrq4/3+eE+5QFGJ3Itc4kvvbw80NIOhFL74NzAH15CuP
AFZ6EHUKNpqZ/9Ek0ppmRAaYrPnBsVqSCvDnRFVL8a3mi6Beul1B2Tixpx0wOKzd
wy5nx/eXEwBXBZJY91CgQDUFNMKMflxlIXgUIpAWUAjEemKlUJojxcmbB3+oNUPz
t8XTqPkcKedrDnLBdmyai6G2CTT187b6gx3u+qX2GT0HY1NG1x9PpH6+0ubL3loP
5GyLf0FkVjBVMgCVx08wRCnYWkTokDobVeDrHDkFiDo=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<35.0.0
Minimum Integer bits32
PHP>=8.2.0
SuiteCRM integration 3.0.0
Release Details
UpdatedJuly 29, 2026, 1:38 p.m.
Changelog

Renames the Nextcloud app id from njordium_suitecrm back to integration_suitecrm. This is the only breaking change in the release. Every setting on your existing 2.x install, admin OAuth config (instance URL, client ID/secret, authorize path), every per-user OAuth token, every widget preference (pipeline_mode, quick_actions_enabled, calendar_show_tasks) carries across automatically via a Repair step that runs on occ upgrade. Users do not need to re-authorise SuiteCRM.

Why the rename. When the fork first shipped in 2.0.0, Julien Veyssier's original integration_suitecrm App Store record was stale and blocked our updates from reaching his ~200 existing installs. The pragmatic response was to rename the fork's app id to njordium_suitecrm so we could ship on the App Store under our own record. In July 2026, Julien transferred ownership of the original record to this fork (see nextcloud/app-certificate-requests#1104 and #1114), which removed the original blocker. 3.0.0 restores the canonical integration_suitecrm app id so:

  • Julien's existing 1.x installs get updates seamlessly from the App Store, no manual reinstall.
  • There's one canonical id on the store, no user confusion about which of two records to install.
  • The GitHub org stays njordium/integration_suitecrm — the App Store app id and the GitHub org name are decoupled.

See docs/upgrade-2.x-to-3.0.md for the end-to-end upgrade walkthrough, rollback procedure, and post-upgrade verification steps.

Changed

  • App id renamed from njordium_suitecrm back to integration_suitecrm. The install folder is now custom_apps/integration_suitecrm/; the old custom_apps/njordium_suitecrm/ will no longer be recognised. Every HTTP route the frontend calls (/apps/integration_suitecrm/*) and every internal string reference has been updated in a single atomic pass across 35 files.
  • occ command name is now occ integration_suitecrm:test-connection. The 2.x form occ njordium_suitecrm:test-connection no longer resolves.
  • OAuth redirect URL on the SuiteCRM OAuth2 client must be updated from <nextcloud>/apps/njordium_suitecrm/oauth-callback to <nextcloud>/apps/integration_suitecrm/oauth-callback. Byte-for-byte match required. Same manual step users hit on the 1.9 to 2.0 upgrade; instructions in docs/upgrade-2.x-to-3.0.md.
  • Webpack bundle filenames flipped from njordium_suitecrm-*.js back to integration_suitecrm-*.js. Direct-install admins with cached JS in a reverse proxy should invalidate the cache on upgrade.
  • Migration\CopyLegacyAppConfig SQL body unchanged, LEGACY_APP_ID constant flipped from integration_suitecrm (2.0.0) to njordium_suitecrm (3.0.0). The class continues to run on every occ upgrade as a post-migration Repair step. On a fresh install (no rows to copy) it's a silent no-op.

Migration path

  • From 2.x (any 2.0-2.6 release) to 3.0.0: occ upgrade runs Migration\CopyLegacyAppConfig which copies every oc_appconfig row where appid='njordium_suitecrm' and every oc_preferences row for the same app id into appid='integration_suitecrm'. Legacy rows are left in place so rollback stays trivial. Encrypted OAuth tokens survive the copy because they live in oc_preferences under app id-scoped rows.
  • From 1.9.x directly to 3.0.0: oc_appconfig and oc_preferences rows are already under integration_suitecrm (that was Julien's app id), so the Repair step is a silent no-op and the deployment picks the settings up under the same id automatically.

Rollback

If 3.0.0 misbehaves, rollback to 2.6.0 is safe because the migration is copy-only, not move: occ app:disable integration_suitecrm && occ app:enable njordium_suitecrm && occ upgrade restores every setting. Detailed steps in docs/upgrade-2.x-to-3.0.md.

Licenses GNU Affero General Public License v3.0 or later
Certificate-----BEGIN CERTIFICATE-----
MIIEDzCCAvcCAhOvMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzI4MTIwMjUxWhcNMzYxMTAyMTIwMjUxWjAfMR0wGwYD
VQQDDBRpbnRlZ3JhdGlvbl9zdWl0ZWNybTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAK0Ns160Hj+trG84ndnLXeskq5GS2HZe/v1g1gS/jhZPi9fshEwm
eDP0YmbbY58HhN3f2TKy1UqsYLezEZ8lDv7V/4nLoeHso8JgQhvGdYqJ9juRu0C+
Ef6y4EhOGempbZxANBPRNYnGDSq/8azygm+bLaPOGp64cCx7Ly1uWeGt91gl3RVy
LeBF+9SFfpiGPDy7bFE8ryM0coA0SPu6i5O2vkLCEDT1jlvpZ1C7xo57hIQW8z9O
O5QTuQQR+JcaZcMZMfevhmcclnQ6mn2rUHgxksz1oyHQC12FbJelDEsuHvW+kowi
WtV4SBEKq2kEOuNn1NoR0bzw8Qn8uaAZ78hpahxwahL5SHrTSBZtzDZECZ3E+0A2
dc5sfhi2RPEgvhcVgp7YLw2YuC+jcbUp1C427Q/L12b+of0aFCUKDLP7sPRgsUVy
m9rVgFw+i2SXu56GMYsSf0RGRDiiUljeHvLZZKyD9gofGQZlrQ9QLncMKZG819cS
dbzwR+hOD+GUt7o5C2xCCuq3OFAyXy36vnE3NimUlmFLgXl3v1K8RDm1db/o7E1H
zadrr4hWEEvd0pDamSQCnTWksHskB2YsERqg11hnEVjVzD6JZ++SUx8glcdiUhNI
LwjFJaqPgS3sK2krz36kyEOOMUQ6GutNszqcoIB8cm5PpVcMVKsRZq9LAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAGEElf1vz20eScuu3ko3VBG+60Rw8UWnRPLIyfXf
69O604BePbo0fAeEvHl3tRc9LGzVmoNoJT107nLBDtFwF6uoiec6SXrO6x4gswQE
oCNYebUrwbFokjBzUASTLigldBiliNIPGFpe9s6bm6Q6Wp0bYnXRNYId2zO4TlCf
nLyB1KHL+FgMqIHnDdRnnmkLuKsEWgzElDLsxvE1y/JnQ6FlpRuX25GU/vqRW1Yq
GVcPd6VsAd8fKGCgexgBTHamCQCUcw+F/S4VGqwyq3QunCUaYeOW/rwXcS7IZHRE
Xy+bqlzii+RL9HBexZC8JyTX5oMUWWDaL8Jv5VcvoM2FtQM=
-----END CERTIFICATE-----
Signatureogxzom4kGvfPpSAHfscVLCco55DbReJH1/78dRCmY1oXJ2XiHb1LEXA33NKJMSEt
OBKUO3MKw31GLqvjOSh/q1nIThsgvMviEnvqSHKf3A3Xd9zeJCW0gtuQsFrFGrIV
qsw16PpfY8qwa/WSpV7i3hVEY38BohCGP+GtlTiTg4OZUouyBkt7c5xqO6PlhXBX
fkIJepB1Lf6NLFa9B67Lxuzn0LLXG9BHamrfhCMvENcoJ3gfEXlbqB8/Isxg7i9D
hzgQD+pR/l6nNgJVwT8oatJpeS4SiarOH2RTMxSIkbBU1d9GgXTRSEZ2zLmU1/do
R2W9vcNWnLfNYpIzWVy9UQNG/OLd7aGGMp6Q5EJSKPQGleKLlCzoVwyb7ghNu6fU
NDeEpJQu9wNmCTEWJKzXrDiENJKAAoWHocLv5CL00xK/yGRTXj3of6NOmUfDkAq5
TfHMQcxnpj7PG103qqaZI/JQ3X49MXDbe/BOnXafNyhEXJVReYHTqXfSk+HTE4dm
VwJjN8cjunijsld5PLDWY3xyK8s/or+5xgikAwm5apcI/Masfk/FGsPWq4NJUXQN
VsQOX5KwNrGdPZmxxMdd5Rov8CkC9eE/Yrtwhem8fd1vs8b0T88PPv/224Fwb4jJ
oI9709FIemcZF3xrQTMPnwOmfKIVKiGVlOsva3IbYl8=
Signature digestsha512
Dependencies
Required Nextcloud versions >=30.0.0,<35.0.0
Minimum Integer bits32
PHP>=8.2.0