( folderPath: string, fileName: string, templatePath: string )
| 527 | } |
| 528 | |
| 529 | protected normalizeTemplateFilePath( |
| 530 | folderPath: string, |
| 531 | fileName: string, |
| 532 | templatePath: string |
| 533 | ): string { |
| 534 | const safeFolderPath = this.stripLeadingSlash(folderPath); |
| 535 | const actualFolderPath: string = safeFolderPath ? `${safeFolderPath}/` : ""; |
| 536 | const extension = this.getTemplateExtension(templatePath); |
| 537 | const normalizedFileName = normalizeGeneratedFilePath( |
| 538 | this.stripLeadingSlash(fileName), |
| 539 | "File name", |
| 540 | ); |
| 541 | const formattedFileName: string = normalizeGeneratedFilePath( |
| 542 | normalizedFileName |
| 543 | .replace(MARKDOWN_FILE_EXTENSION_REGEX, "") |
| 544 | .replace(CANVAS_FILE_EXTENSION_REGEX, "") |
| 545 | .replace(BASE_FILE_EXTENSION_REGEX, ""), |
| 546 | "File name", |
| 547 | ); |
| 548 | // Validate the final path segment, not just the whole string — a |
| 549 | // trailing-slash name like "Projects/" (optional leaf token left |
| 550 | // empty) would otherwise still produce "Projects/.md". |
| 551 | const baseName = formattedFileName.slice( |
| 552 | formattedFileName.lastIndexOf("/") + 1 |
| 553 | ); |
| 554 | if (!baseName.trim()) { |
| 555 | throw new Error( |
| 556 | "File name is empty after formatting. Make sure the tokens in the file name format produce a value (an optional token left empty can cause this)." |
| 557 | ); |
| 558 | } |
| 559 | const assembledPath = `${actualFolderPath}${formattedFileName}${extension}`; |
| 560 | // Contain the assembled target at this shared chokepoint. The file NAME is run |
| 561 | // through normalizeGeneratedFilePath above, but the FOLDER portion is only |
| 562 | // stripLeadingSlash'd — so a folder like "../../../evil" (from an untrusted, |
| 563 | // synced Template choice resolved via formatFolderPath) would assemble an |
| 564 | // out-of-vault path. Both callers act on it without a sink guard: the relocation |
| 565 | // flow (computeChoiceTargetPath -> createFolder + fileManager.renameFile) would |
| 566 | // otherwise move the active note OUTSIDE the vault. Reject any escape here so |
| 567 | // every caller of this assembler is contained. |
| 568 | if (escapesVaultBoundary(assembledPath)) { |
| 569 | throw new Error( |
| 570 | `Refusing to build a file path outside the vault: "${assembledPath}".`, |
| 571 | ); |
| 572 | } |
| 573 | return assembledPath; |
| 574 | } |
| 575 | |
| 576 | protected async createFileWithTemplate( |
| 577 | filePath: string, |
nothing calls this directly
no test coverage detected