@jwilleke/ngdpbase-types 5.43.0 → 5.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/lib/src/context/ApiContext.d.ts +10 -3
  2. package/lib/src/context/ApiContext.js +36 -3
  3. package/lib/src/ice/runExport.d.ts +8 -3
  4. package/lib/src/ice/searchExportSource.d.ts +44 -0
  5. package/lib/src/ice/types.d.ts +6 -0
  6. package/lib/src/managers/AttachmentManager.d.ts +11 -0
  7. package/lib/src/managers/AuditManager.d.ts +0 -1
  8. package/lib/src/managers/AuthManager.d.ts +38 -0
  9. package/lib/src/managers/AuthManager.js +84 -12
  10. package/lib/src/managers/BackupManager.d.ts +9 -2
  11. package/lib/src/managers/BackupManager.js +42 -19
  12. package/lib/src/managers/BaseManager.d.ts +17 -3
  13. package/lib/src/managers/BaseManager.js +12 -8
  14. package/lib/src/managers/ConfigurationManager.d.ts +13 -0
  15. package/lib/src/managers/ConsentManager.d.ts +1 -1
  16. package/lib/src/managers/DatabaseManager.js +17 -8
  17. package/lib/src/managers/EmailManager.d.ts +1 -1
  18. package/lib/src/managers/ExportManager.d.ts +1 -44
  19. package/lib/src/managers/ExportManager.js +3 -115
  20. package/lib/src/managers/ImportManager.d.ts +7 -0
  21. package/lib/src/managers/PageManager.d.ts +9 -0
  22. package/lib/src/managers/PolicyEvaluator.d.ts +113 -30
  23. package/lib/src/managers/ThemeManager.d.ts +20 -1
  24. package/lib/src/providers/FileLoggingProvider.d.ts +12 -1
  25. package/lib/src/providers/FileLoggingProvider.js +37 -4
  26. package/lib/src/security/PolicyDecisionPoint.d.ts +50 -3
  27. package/lib/src/security/PolicyInformationPoint.d.ts +27 -1
  28. package/lib/src/security/ownedRecords.d.ts +69 -0
  29. package/lib/src/security/signedOutWrites.d.ts +69 -0
  30. package/lib/src/security/signedOutWrites.js +146 -0
  31. package/lib/src/types/Policy.d.ts +26 -30
  32. package/lib/src/utils/addonConfigLayer.d.ts +6 -3
  33. package/lib/src/utils/addonConfigLayer.js +61 -3
  34. package/lib/src/utils/addonHost.d.ts +25 -0
  35. package/lib/src/utils/addonHost.js +29 -1
  36. package/lib/src/utils/availability.d.ts +15 -0
  37. package/lib/src/utils/availability.js +88 -0
  38. package/lib/src/utils/pageTitleConflict.d.ts +32 -0
  39. package/lib/src/utils/pageWritePolicies.d.ts +48 -29
  40. package/lib/src/utils/plainAttention.d.ts +19 -0
  41. package/lib/src/utils/plainAttention.js +18 -0
  42. package/lib/src/utils/policyAttributes.d.ts +113 -22
  43. package/lib/src/utils/policyAttributes.js +228 -43
  44. package/lib/src/utils/roleGrants.d.ts +1 -1
  45. package/package.json +7 -7
@@ -282,7 +282,21 @@ export interface ManagerStatus {
282
282
  configKey?: string;
283
283
  /** Whether the feature is down or works but needs an admin (#1946). Present when degraded or failed. */
284
284
  kind?: DegradedKind;
285
+ /** Where the fix is, when it is not the setting `configKey` names (a record the app writes, not a value to edit). */
286
+ fixUrl?: string;
287
+ /** What the link to `fixUrl` says, e.g. "Open Admin → Backup". */
288
+ fixLabel?: string;
289
+ /** One line per problem, drawn as a list under `reason` (#2348). `reason` stays the headline and the audit text. */
290
+ details?: string[];
291
+ /** Buttons under the item (#2355): each posts a form to `action` with `fields`; the page adds the CSRF token. Labels and values are text. */
292
+ buttons?: Array<{
293
+ label: string;
294
+ action: string;
295
+ fields: Record<string, string>;
296
+ }>;
285
297
  }
298
+ /** What a caller may add to a needs-attention status (#2348). */
299
+ export type AttentionOptions = Pick<ManagerStatus, 'fixUrl' | 'fixLabel' | 'details' | 'buttons'>;
286
300
  declare abstract class BaseManager {
287
301
  /** Reference to the wiki engine */
288
302
  protected engine: WikiEngine;
@@ -425,18 +439,18 @@ declare abstract class BaseManager {
425
439
  * starts degraded and stays degraded must not re-emit on every boot, or
426
440
  * the signal is buried in its own noise.
427
441
  */
428
- protected markDegraded(reason: string, configKey?: string): boolean;
442
+ protected markDegraded(reason: string, configKey?: string, opts?: AttentionOptions): boolean;
429
443
  /**
430
444
  * Record that this manager works, but an administrator has something to do
431
445
  * (#1946). Still `degraded` — it stays on the dashboard, is audited, and
432
446
  * clears with `markReadyIfDegradedBy` — but shown as needing attention, not
433
447
  * as down.
434
448
  */
435
- protected markNeedsAttention(reason: string, configKey?: string): boolean;
449
+ protected markNeedsAttention(reason: string, configKey?: string, opts?: AttentionOptions): boolean;
436
450
  /** Record that this manager is deliberately off. Not a problem (#1155). */
437
451
  protected markDisabled(reason: string): boolean;
438
452
  /** Record that this manager could not initialise at all (#1155). */
439
- protected markFailed(reason: string, configKey?: string): boolean;
453
+ protected markFailed(reason: string, configKey?: string, opts?: AttentionOptions): boolean;
440
454
  /**
441
455
  * Record that this manager is working.
442
456
  *
@@ -18,6 +18,7 @@ import logger from '../utils/logger.js';
18
18
  import { checkConfiguredPath } from '../utils/PathPreflight.js';
19
19
  import { recordAuditEvent } from '../utils/auditEvents.js';
20
20
  import { AUDIT_EVENT } from '../utils/auditEventNames.js';
21
+ import { STAGED_RESTORE_FIX, stagedRestoreNotApplied, UNUSABLE_FOLDER_REASON } from '../utils/plainAttention.js';
21
22
  class BaseManager {
22
23
  /** Reference to the wiki engine */
23
24
  engine;
@@ -121,7 +122,9 @@ class BaseManager {
121
122
  const { applyStagedPartsAtStart } = await import('./BackupManager.js');
122
123
  this.stagedRestore = applyStagedPartsAtStart(this.engine, this.registeredName(), declared);
123
124
  if (this.stagedRestore?.refused) {
124
- this.markNeedsAttention(`A staged restore of ${declared.map((d) => d.part).join(', ')} was not applied: ${this.stagedRestore.refused}`);
125
+ // #2354: the dashboard says it in plain words; the parts and the cause stay in the log.
126
+ logger.warn(`[${this.constructor.name}] A staged restore of ${declared.map((d) => d.part).join(', ')} was not applied: ${this.stagedRestore.refused}`);
127
+ this.markNeedsAttention(stagedRestoreNotApplied('some of the site\'s data'), undefined, STAGED_RESTORE_FIX);
125
128
  }
126
129
  }
127
130
  /**
@@ -193,7 +196,8 @@ class BaseManager {
193
196
  // degrading manager already goes through, so setting the state here is
194
197
  // what makes twelve invisible degradations visible without each caller
195
198
  // remembering to say so.
196
- this.markDegraded(result.message ?? 'the configured path could not be used', configKey);
199
+ // #2354: the dashboard says it in plain words; the path and key are in the line above.
200
+ this.markDegraded(UNUSABLE_FOLDER_REASON, configKey);
197
201
  }
198
202
  return result;
199
203
  }
@@ -216,8 +220,8 @@ class BaseManager {
216
220
  * starts degraded and stays degraded must not re-emit on every boot, or
217
221
  * the signal is buried in its own noise.
218
222
  */
219
- markDegraded(reason, configKey) {
220
- return this.transition({ state: 'degraded', reason, configKey, kind: 'outage' });
223
+ markDegraded(reason, configKey, opts = {}) {
224
+ return this.transition({ state: 'degraded', reason, configKey, kind: 'outage', ...opts });
221
225
  }
222
226
  /**
223
227
  * Record that this manager works, but an administrator has something to do
@@ -225,16 +229,16 @@ class BaseManager {
225
229
  * clears with `markReadyIfDegradedBy` — but shown as needing attention, not
226
230
  * as down.
227
231
  */
228
- markNeedsAttention(reason, configKey) {
229
- return this.transition({ state: 'degraded', reason, configKey, kind: 'attention' });
232
+ markNeedsAttention(reason, configKey, opts = {}) {
233
+ return this.transition({ state: 'degraded', reason, configKey, kind: 'attention', ...opts });
230
234
  }
231
235
  /** Record that this manager is deliberately off. Not a problem (#1155). */
232
236
  markDisabled(reason) {
233
237
  return this.transition({ state: 'disabled', reason });
234
238
  }
235
239
  /** Record that this manager could not initialise at all (#1155). */
236
- markFailed(reason, configKey) {
237
- return this.transition({ state: 'failed', reason, configKey, kind: 'outage' });
240
+ markFailed(reason, configKey, opts = {}) {
241
+ return this.transition({ state: 'failed', reason, configKey, kind: 'outage', ...opts });
238
242
  }
239
243
  /**
240
244
  * Record that this manager is working.
@@ -54,6 +54,8 @@ declare class ConfigurationManager extends BaseManager {
54
54
  private defaultConfig;
55
55
  private customConfig;
56
56
  private mergedConfig;
57
+ /** #2279: counts every change to `mergedConfig`, so a reader may keep what it derived from a value until the next one. */
58
+ private revision;
57
59
  /** #1638: shipped ⊕ enabled addons, before the operator's file — what the build declares. */
58
60
  private declaredConfig;
59
61
  /** #1813: each enabled addon's own default-config.json, as folded into the merge. */
@@ -200,8 +202,19 @@ declare class ConfigurationManager extends BaseManager {
200
202
  * @throws {Error} If default configuration file cannot be loaded
201
203
  */
202
204
  private loadConfigurations;
205
+ /**
206
+ * A number that changes whenever the configuration in force changes (#2279):
207
+ * a load or reload, a save, a reset, a restore, a runtime value an add-on
208
+ * sets. Equal numbers mean nothing was changed through this manager, so
209
+ * what a reader derived from `getProperty` then still holds. A value
210
+ * returned by `getProperty` must not be changed in place; that is the one
211
+ * change this cannot see.
212
+ */
213
+ getRevision(): number;
203
214
  /** What the #2255 page-write migration rewrote in memory, one sentence each; PolicyDecisionPoint reports it. */
204
215
  pageWritePolicyMigration: string[];
216
+ /** Each `pageWritePolicyMigration` line in plain words, for the needs-attention entry. */
217
+ pageWritePolicyMigrationShown: Record<string, string>;
205
218
  /**
206
219
  * #2255: before the merge, rewrite the custom layer's stored copies of the
207
220
  * shipped role policies whose page writes moved to the category policies
@@ -56,7 +56,7 @@ declare class ConsentManager extends BaseManager {
56
56
  private readonly addons;
57
57
  /** Username → their records, read once and dropped on every write. */
58
58
  private readonly cache;
59
- /** Document id → why it cannot be asked for; the needs-attention reason is made from these. */
59
+ /** Document id → why it cannot be asked for (the needs-attention reason is made from these), and whether it comes with an add-on (core's Required Pages Sync does not restore those). */
60
60
  private readonly problems;
61
61
  private attentionReason;
62
62
  constructor(engine: WikiEngine);
@@ -50,6 +50,7 @@ import { applyStagedRestore, manifestPath, runOffThread, setAsideStagedRestore,
50
50
  import { BACKUP_KEY_ENV, backupKeyFingerprint, currentBackupKey, LEFT_OUT_FOR_NO_KEY } from '../utils/backupKey.js';
51
51
  // The environment variable holding the SQLCipher key: one declaration, `src/utils/databaseKey.ts`.
52
52
  import { DATABASE_KEY_ENV } from '../utils/databaseKey.js';
53
+ import { STAGED_RESTORE_FIX } from '../utils/plainAttention.js';
53
54
  export { DATABASE_KEY_ENV };
54
55
  // #1723, #1876: the backup key has one declaration (`src/utils/backupKey.ts`); re-exported for the callers that import it from here.
55
56
  export { BACKUP_KEY_ENV, backupKeyFingerprint } from '../utils/backupKey.js';
@@ -138,7 +139,7 @@ class DatabaseManager extends BaseManager {
138
139
  catch (err) {
139
140
  this.markUnavailable(`A staged database restore could not be put in place: ${err.message}. ` +
140
141
  'No database was opened. Some databases may already be restored: each one moved is kept beside it as *.pre-restore, and the rest are still *.staged beside ' +
141
- `${path.basename(manifestPath(file))}. Stop the server, put the files back as they should be, and restart (#1892)`);
142
+ `${path.basename(manifestPath(file))}. Stop the server, put the files back as they should be, and restart (#1892)`, 'A saved copy of the databases was being put back and it did not finish, so the site is in maintenance mode and no database is open. Some may already have been replaced. Ask whoever runs the server to check the database files and restart the site; this is done on the server, outside this page.');
142
143
  return;
143
144
  }
144
145
  // #1892: a database that cannot be opened safely (no key or the wrong key
@@ -151,7 +152,7 @@ class DatabaseManager extends BaseManager {
151
152
  provider = new SqliteDatabaseProvider(file, key, DATABASE_MIGRATIONS, 'schema_migrations', { busyTimeoutMs: this.busyTimeoutMs });
152
153
  }
153
154
  catch (err) {
154
- this.markUnavailable(DatabaseManager.openFailure(file, err.message, key));
155
+ this.markUnavailable(DatabaseManager.openFailure(file, err.message, key), 'The database could not be opened safely, so the site is in maintenance mode. If its key is missing or wrong, fix it in the server\'s settings (outside this page) and restart the site. If the file is damaged, restore the databases from a backup.', STAGED_RESTORE_FIX);
155
156
  return;
156
157
  }
157
158
  this.provider = provider;
@@ -195,11 +196,12 @@ class DatabaseManager extends BaseManager {
195
196
  * administrators can sign in and reach /admin, and `reportAtBoot()` sends
196
197
  * them the reason as a critical notification once notifications exist.
197
198
  */
198
- markUnavailable(reason) {
199
+ markUnavailable(reason, plain, fix) {
199
200
  this.unavailable = reason;
200
201
  this.provider = null;
201
202
  logger.error(`[DatabaseManager] ${reason}`);
202
- this.markFailed(reason, 'ngdpbase.database.file');
203
+ // #2354: the dashboard says it in plain words; the developer text is the log line above.
204
+ this.markFailed(plain, 'ngdpbase.database.file', fix);
203
205
  // #1906: no message of its own, so visitors see the site's maintenance message.
204
206
  this.engine.maintenance.enter(reason);
205
207
  }
@@ -690,7 +692,13 @@ class DatabaseManager extends BaseManager {
690
692
  }
691
693
  /** Degrade for one cause; the dashboard shows every cause still standing. */
692
694
  fault(cause, reason, kind) {
693
- this.faults.set(cause, { reason, kind });
695
+ // #2354: the dashboard shows the plain words; `reason` is the developer text, already logged by the caller.
696
+ const plain = cause === 'restore'
697
+ ? 'A saved copy of the databases was not put back when the site started, so the databases that were there before are still in use. Open the Backup page to see what to do next.'
698
+ : this.backupKey()
699
+ ? "The backup key is the same as the database key, so one stolen secret would open both. Give the backups a key of their own, in the server's settings, outside this page."
700
+ : "Databases are not being backed up, because the backup key is not set. This is set in the server's settings, outside this page.";
701
+ this.faults.set(cause, { reason, plain, kind });
694
702
  this.markFaults();
695
703
  }
696
704
  clearFault(cause) {
@@ -704,11 +712,12 @@ class DatabaseManager extends BaseManager {
704
712
  /** Every cause still standing; down when any one of them is (#1946). */
705
713
  markFaults() {
706
714
  const faults = [...this.faults.values()];
707
- const reason = faults.map((f) => f.reason).join('; ');
715
+ const reason = faults.map((f) => f.plain).join(' ');
716
+ const fix = this.faults.has('restore') ? STAGED_RESTORE_FIX : {};
708
717
  if (faults.some((f) => f.kind === 'outage'))
709
- this.markDegraded(reason);
718
+ this.markDegraded(reason, undefined, fix);
710
719
  else
711
- this.markNeedsAttention(reason);
720
+ this.markNeedsAttention(reason, undefined, fix);
712
721
  }
713
722
  async notify(level, title, message) {
714
723
  try {
@@ -35,7 +35,7 @@ import type { MailAttachment, MailMessage, MailProvider } from '../mail/MailProv
35
35
  import type { ActorContext } from '../context/ActorContext.js';
36
36
  export type { MailAttachment } from '../mail/MailProvider.js';
37
37
  /** The degraded reason while sends fail; cleared by the next success. */
38
- export declare const SEND_FAILING_REASON = "outbound email is failing \u2014 see the server log and the administrator notification";
38
+ export declare const SEND_FAILING_REASON = "The site can't send email right now. Check the email settings.";
39
39
  /** Title of the administrator notice. */
40
40
  export declare const SEND_FAILING_TITLE = "Email could not be sent";
41
41
  /** An attachment the message cannot carry (#1974): refused before sending, so it is not a delivery failure. */
@@ -10,17 +10,7 @@ export type { PdfOptions, PdfMargin, PdfPageSize } from '../export/pdfEngine.js'
10
10
  * when something asked for a PDF and could not have one; cleared by the next
11
11
  * render that works.
12
12
  */
13
- export declare const PDF_UNAVAILABLE_REASON = "PDF export unavailable: choose a PDF provider (local-chrome or chrome-service) in ngdpbase.export.pdf.provider, or fix the one chosen (the server log says why)";
14
- /**
15
- * Export file metadata interface
16
- */
17
- export interface ExportFileInfo {
18
- filename: string;
19
- path: string;
20
- size: number;
21
- created: Date;
22
- modified: Date;
23
- }
13
+ export declare const PDF_UNAVAILABLE_REASON = "PDFs can't be made right now. Choose how PDFs are made in the settings, or fix the one already chosen.";
24
14
  /**
25
15
  * Export configuration interface
26
16
  */
@@ -146,13 +136,6 @@ declare class ExportManager extends BaseManager {
146
136
  * @returns {Promise<void>}
147
137
  */
148
138
  initialize(config?: ExportConfig): Promise<void>;
149
- /**
150
- * Export a single page to HTML
151
- * @param {string} pageName - Page name to export
152
- * @param {ExportUser|null} user - User object for locale-aware formatting
153
- * @returns {Promise<string>} HTML content
154
- */
155
- exportPageToHtml(pageName: string, ctx: ActorContext, user?: ExportUser | null): Promise<string>;
156
139
  /**
157
140
  * One page as a complete HTML document: its content rendered, with the
158
141
  * export's styling and metadata footer. It reads nothing: the caller has
@@ -171,32 +154,6 @@ declare class ExportManager extends BaseManager {
171
154
  * @returns {Promise<string>} Combined HTML content
172
155
  */
173
156
  exportPagesToHtml(pageNames: string[], ctx: ActorContext, user?: ExportUser | null): Promise<string>;
174
- /**
175
- * Export page(s) to markdown
176
- * @param {string|string[]} pageNames - Single page name or array of page names
177
- * @param {ExportUser|null} user - User object for locale-aware formatting
178
- * @returns {Promise<string>} Markdown content
179
- */
180
- exportToMarkdown(pageNames: string | string[], ctx: ActorContext, user?: ExportUser | null): Promise<string>;
181
- /**
182
- * Save export to file
183
- * @param {string} content - Content to save
184
- * @param {string} filename - Filename
185
- * @param {string} format - File format
186
- * @returns {Promise<string>} File path
187
- */
188
- saveExport(content: string, filename: string, format: string): Promise<string>;
189
- /**
190
- * Get list of available exports
191
- * @returns {Promise<ExportFileInfo[]>} List of export files
192
- */
193
- getExports(): Promise<ExportFileInfo[]>;
194
- /**
195
- * Delete an export file
196
- * @param {string} filename - Export filename
197
- * @returns {Promise<void>}
198
- */
199
- deleteExport(filename: string): Promise<void>;
200
157
  /**
201
158
  * Get formatted timestamp using user's locale
202
159
  * @param {ExportUser|null} user - User object (optional)
@@ -1,6 +1,5 @@
1
1
  import BaseManager from './BaseManager.js';
2
2
  import fs from 'fs/promises';
3
- import path from 'path';
4
3
  import LocaleUtils from '../utils/LocaleUtils.js';
5
4
  import logger from '../utils/logger.js';
6
5
  import { actorOf } from '../context/ActorContext.js';
@@ -15,7 +14,7 @@ export { PdfUnavailableError };
15
14
  * when something asked for a PDF and could not have one; cleared by the next
16
15
  * render that works.
17
16
  */
18
- export const PDF_UNAVAILABLE_REASON = 'PDF export unavailable: choose a PDF provider (local-chrome or chrome-service) in ngdpbase.export.pdf.provider, or fix the one chosen (the server log says why)';
17
+ export const PDF_UNAVAILABLE_REASON = "PDFs can't be made right now. Choose how PDFs are made in the settings, or fix the one already chosen.";
19
18
  /** Shipped defaults of the `ngdpbase.export.pdf.*` keys. */
20
19
  const PDF_DEFAULTS = { provider: 'none', pageSize: 'Letter', timeoutMs: 30_000, maxConcurrent: 2, serviceMaxBytes: 52_428_800 };
21
20
  /**
@@ -34,7 +33,7 @@ const PDF_DEFAULTS = { provider: 'none', pageSize: 'Letter', timeoutMs: 30_000,
34
33
  *
35
34
  * @example
36
35
  * const exportManager = engine.getManager('ExportManager');
37
- * const html = await exportManager.exportPageToHtml('Main');
36
+ * const csv = exportManager.toCsv(['name'], [['Main']]);
38
37
  */
39
38
  /** A text cell starting with one of these runs as a spreadsheet formula. */
40
39
  const CSV_FORMULA_START = /^[=+\-@\t\r]/;
@@ -229,7 +228,7 @@ class ExportManager extends BaseManager {
229
228
  this.exportDirectory = config.exportDirectory
230
229
  || this.engine.getManager('ConfigurationManager')?.getResolvedDataPath('ngdpbase.directories.exports', '${FAST_STORAGE}/exports')
231
230
  || './exports';
232
- const preflight = this.preflightConfiguredPath('exportDirectory', this.exportDirectory);
231
+ const preflight = this.preflightConfiguredPath('ngdpbase.directories.exports', this.exportDirectory);
233
232
  if (!preflight.ok) {
234
233
  this.exportDirectory = '';
235
234
  logger.info('ExportManager initialized (degraded — exports disabled)');
@@ -240,24 +239,6 @@ class ExportManager extends BaseManager {
240
239
  const pdfProviderName = this.engine.getManager('ConfigurationManager')?.getProperty('ngdpbase.export.pdf.provider', PDF_DEFAULTS.provider);
241
240
  logger.info(`ExportManager initialized (PDF provider: ${pdfProviderName ?? PDF_DEFAULTS.provider})`);
242
241
  }
243
- /**
244
- * Export a single page to HTML
245
- * @param {string} pageName - Page name to export
246
- * @param {ExportUser|null} user - User object for locale-aware formatting
247
- * @returns {Promise<string>} HTML content
248
- */
249
- async exportPageToHtml(pageName, ctx, user = null) {
250
- const pageManager = this.engine.getManager('PageManager');
251
- const renderingManager = this.engine.getManager('RenderingManager');
252
- if (!pageManager || !renderingManager) {
253
- throw new Error('Required managers not available');
254
- }
255
- const page = (await pageManager.getPage(pageName, ctx));
256
- if (!page) {
257
- throw new Error(`Page '${pageName}' not found`);
258
- }
259
- return this.renderPageDocument(pageName, page, user);
260
- }
261
242
  /**
262
243
  * One page as a complete HTML document: its content rendered, with the
263
244
  * export's styling and metadata footer. It reads nothing: the caller has
@@ -480,99 +461,6 @@ class ExportManager extends BaseManager {
480
461
  </html>`;
481
462
  return html;
482
463
  }
483
- /**
484
- * Export page(s) to markdown
485
- * @param {string|string[]} pageNames - Single page name or array of page names
486
- * @param {ExportUser|null} user - User object for locale-aware formatting
487
- * @returns {Promise<string>} Markdown content
488
- */
489
- async exportToMarkdown(pageNames, ctx, user = null) {
490
- const pageManager = this.engine.getManager('PageManager');
491
- if (!pageManager) {
492
- throw new Error('PageManager not available');
493
- }
494
- const names = Array.isArray(pageNames) ? pageNames : [pageNames];
495
- let markdown = '';
496
- if (names.length > 1) {
497
- markdown += '# ngdpbase Export\n\n';
498
- markdown += `*Exported on ${this.getFormattedTimestamp(user)}*\n\n`;
499
- markdown += '## Table of Contents\n\n';
500
- for (const pageName of names) {
501
- markdown += `- [${pageName}](#${pageName.toLowerCase().replace(/[^a-z0-9]/g, '-')})\n`;
502
- }
503
- markdown += '\n---\n\n';
504
- }
505
- for (const pageName of names) {
506
- const page = (await pageManager.getPage(pageName, ctx));
507
- if (page) {
508
- if (names.length > 1) {
509
- markdown += `# ${pageName}\n\n`;
510
- }
511
- markdown += page.content;
512
- if (names.length > 1) {
513
- markdown += '\n\n---\n\n';
514
- }
515
- }
516
- }
517
- return markdown;
518
- }
519
- /**
520
- * Save export to file
521
- * @param {string} content - Content to save
522
- * @param {string} filename - Filename
523
- * @param {string} format - File format
524
- * @returns {Promise<string>} File path
525
- */
526
- async saveExport(content, filename, format) {
527
- const sanitizedFilename = filename.replace(/[^a-zA-Z0-9\-_]/g, '-');
528
- const timestamp = new Date().toISOString().slice(0, 10);
529
- const fullFilename = `${sanitizedFilename}_${timestamp}.${format}`;
530
- const filePath = path.join(this.exportDirectory, fullFilename);
531
- await fs.writeFile(filePath, content, 'utf8');
532
- logger.info(`Exported to: ${fullFilename}`);
533
- return filePath;
534
- }
535
- /**
536
- * Get list of available exports
537
- * @returns {Promise<ExportFileInfo[]>} List of export files
538
- */
539
- async getExports() {
540
- try {
541
- const files = await fs.readdir(this.exportDirectory);
542
- const exports = [];
543
- for (const file of files) {
544
- const filePath = path.join(this.exportDirectory, file);
545
- const stats = await fs.stat(filePath);
546
- exports.push({
547
- filename: file,
548
- path: filePath,
549
- size: stats.size,
550
- created: stats.birthtime,
551
- modified: stats.mtime
552
- });
553
- }
554
- return exports.sort((a, b) => b.created.getTime() - a.created.getTime());
555
- }
556
- catch (err) {
557
- logger.error('Error getting exports:', err);
558
- return [];
559
- }
560
- }
561
- /**
562
- * Delete an export file
563
- * @param {string} filename - Export filename
564
- * @returns {Promise<void>}
565
- */
566
- async deleteExport(filename) {
567
- // Only a plain file name inside the export folder: never a path that
568
- // reaches another folder.
569
- if (!filename || filename !== path.basename(filename) || filename === '.' || filename === '..') {
570
- throw new Error(`Not an export file name: ${filename}`);
571
- }
572
- const filePath = path.join(this.exportDirectory, filename);
573
- await fs.unlink(filePath);
574
- logger.info(`Deleted export: ${filename}`);
575
- }
576
464
  /**
577
465
  * Get formatted timestamp using user's locale
578
466
  * @param {ExportUser|null} user - User object (optional)
@@ -633,6 +633,13 @@ declare class ImportManager extends BaseManager {
633
633
  * is permitted, and a refusal to someone who holds it is step-up's.
634
634
  */
635
635
  private mayExportPage;
636
+ /**
637
+ * `core.search` as an export source: a search's pages and files as one zip
638
+ * ({@link searchExportSource}). Like a page, a search is offered to a user
639
+ * who holds `page-export`, and read as the user who asked, each page and
640
+ * file by its own read check. Recorded as one `page-export` of the search.
641
+ */
642
+ private searchExportSource;
636
643
  /**
637
644
  * `core.pages` as an export source (#2120, D5: pages export checks
638
645
  * `page-export`). A page is offered for export when its reader may see it
@@ -1226,6 +1226,15 @@ declare class PageManager extends BaseManager implements CatalogSource {
1226
1226
  title: string;
1227
1227
  uuid: string;
1228
1228
  }>>;
1229
+ /**
1230
+ * Every public page stamped with one system-category (#2299), by the name
1231
+ * `mayShowPage` takes. Vault pages are not listed: they are their owners', not
1232
+ * an administrator's to be told about. Says nothing about who may see a page;
1233
+ * the caller asks `mayShowPage` as the reader.
1234
+ *
1235
+ * @param category - The category's key or label, any case
1236
+ */
1237
+ listCategoryPages(category: string): Promise<string[]>;
1229
1238
  /** The store ids the requester has of their own (#1387). */
1230
1239
  listOwnStoreIds(ctx: ActorContext): Promise<string[]>;
1231
1240
  /**