mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 11:02:33 -05:00
Decided on 2026-10-14. On the import page each of the six asks for what its service needs - a self-hosted Planka its address, username and password; the others an access token (Basecamp also its account id) - lists what that account can import, and fetches the chosen board, list or project. The credentials go with each call and are used for that call's requests only; they are never stored or logged, and the page clears secret fields once a fetch has used them. The fetched JSON fills the import text box and the import goes on as for a pasted export: the service's parser (models/lib/<key>ApiFormat.js) into the generalized importer, with the members step and the loss report. The same JSON can be pasted, or posted to POST /api/boards/import/<key>. - server/apiImport.js: apiImportList and apiImportFetch, signed in only, off with the Admin Panel's import switch, not in Sandstorm, rate limited; credentials checked against the service's own fields (models/lib/apiImportServices.js). - server/lib/apiImportRequest.js: every request through the SSRF guard - pinned address, no redirects, bounded in time and size - so a Planka address cannot point the server at its own network; an administrator can allow such a host with API_IMPORT_ALLOWED_PRIVATE_HOSTS. An error names only the origin and the status. - models/lib/apiImport/<key>.js: each service's client, plain Node with the request injected, paging bounded and the token only in a header. - Each has a docs page under docs/Features/ImportExport/, listed in the coverage index and External-Tools.md. Tests: tests/<key>ApiImport.test.cjs (client with a fake request, every parsed field, wrong shapes), tests/apiImport.test.cjs (credentials, the gate, the wiring, the shapes refused), api-import.e2e.js (the form for every service, fetched JSON imported through the page, refusals incl. a private address). Live tests, read only, run from build.sh / build.bat -> Tests -> Live tests with your own accounts (releases/live-tests/README.md); they were not run here. New strings translated in all locales. Also: the release risk baseline allows the services' API addresses, and the guards that count import creators, list the import sources and pin the platform fetch() calls know the new ones. Thanks to xet7 !
96 lines
6.0 KiB
JavaScript
96 lines
6.0 KiB
JavaScript
// Every import source, described once: what the import page offers for it,
|
|
// what its file is sent as, and which importer creates the board. The import
|
|
// page, "Import many boards" (models/lib/importManyFiles.js) and the server
|
|
// read this instead of each keeping a list of its own.
|
|
//
|
|
// key - the source's name everywhere: the page's address
|
|
// (/import/<key>), importBoard's source and the REST route
|
|
// POST /api/boards/import/<key>
|
|
// name - shown on the page; `product: true` shows this WeKan's own
|
|
// product name instead
|
|
// files - the file extensions the page's file chooser accepts; none
|
|
// means the source is pasted only
|
|
// paste - whether its text can be pasted into the page's text box
|
|
// send - what a chosen file is sent as:
|
|
// 'text' its text (TEXT_SOURCES of importManyFiles)
|
|
// 'json' its parsed JSON
|
|
// 'excel' { excelBase64 } (Plane's workbook: { xlsxBase64 })
|
|
// 'zip' { zipBase64 } - the .zip is ONE export of the tool
|
|
// 'rows' CSV/TSV rows
|
|
// 'own' the source's own upload route (WeKan .zip, Trello .zip)
|
|
// creator - 'generalized' for models/kanboardCreator.js through
|
|
// models/lib/externalParsers.js, else the source's own creator;
|
|
// only the generalized importer can make one board per project
|
|
// package - a .zip of many boards with their files, uploaded to a route
|
|
// of its own (Trello's)
|
|
|
|
const JSON_FILE = ['.json'];
|
|
const TEXT = { paste: true, send: 'text', creator: 'generalized' };
|
|
const JSON_SOURCE = { paste: true, files: JSON_FILE, send: 'json', creator: 'generalized' };
|
|
const EXCEL = { files: ['.xlsx'], send: 'excel', creator: 'generalized' };
|
|
|
|
export const IMPORT_SOURCES = [
|
|
{ key: 'wekan', product: true, paste: true, files: ['.json', '.zip'], send: 'json', zipSend: 'own', creator: 'wekan' },
|
|
{ key: 'trello', name: 'Trello', paste: true, files: JSON_FILE, send: 'json', creator: 'trello', package: true },
|
|
{ key: 'csv', name: 'CSV / TSV', paste: true, files: ['.csv', '.tsv', '.txt'], send: 'rows', creator: 'csv' },
|
|
{ key: 'excel', name: 'Excel', files: ['.xlsx'], send: 'excel', creator: 'csv' },
|
|
{ key: 'jira', name: 'Jira', paste: true, files: JSON_FILE, send: 'json', creator: 'jira' },
|
|
{ key: 'kanboard', name: 'Kanboard', ...JSON_SOURCE },
|
|
{ key: 'deck', name: 'NextCloud Deck', ...JSON_SOURCE },
|
|
{ key: 'openproject', name: 'OpenProject', ...JSON_SOURCE },
|
|
{ key: 'github', name: 'GitHub', ...JSON_SOURCE },
|
|
{ key: 'gitlab', name: 'GitLab', ...JSON_SOURCE },
|
|
{ key: 'gitea', name: 'Gitea', ...JSON_SOURCE },
|
|
{ key: 'forgejo', name: 'Forgejo', ...JSON_SOURCE },
|
|
{ key: 'asana', name: 'Asana', ...JSON_SOURCE },
|
|
{ key: 'zenkit', name: 'Zenkit', ...JSON_SOURCE },
|
|
{ key: 'markdown', name: 'Markdown', ...TEXT, files: ['.md', '.markdown', '.txt'] },
|
|
{ key: 'leo', name: 'Leo', ...TEXT, files: ['.leo'] },
|
|
{ key: 'opml', name: 'OPML', ...TEXT, files: ['.opml', '.xml'] },
|
|
{ key: 'orgmode', name: 'Org mode', ...TEXT, files: ['.org'] },
|
|
{ key: 'todotxt', name: 'todo.txt', ...TEXT, files: ['.txt'] },
|
|
{ key: 'taskwarrior', name: 'Taskwarrior', ...TEXT, files: ['.json'] },
|
|
{ key: 'focalboard', name: 'Focalboard', ...TEXT, files: ['.jsonl', '.json'] },
|
|
{ key: 'todoist', name: 'Todoist', ...TEXT, files: ['.csv'] },
|
|
{ key: 'planner', name: 'Microsoft Planner', ...EXCEL },
|
|
{ key: 'meistertask', name: 'MeisterTask', ...TEXT, files: ['.csv'] },
|
|
{ key: 'obsidian', name: 'Obsidian Kanban', ...TEXT, files: ['.md'] },
|
|
{ key: 'linear', name: 'Linear', ...TEXT, files: ['.csv'] },
|
|
{ key: 'ticktick', name: 'TickTick', ...TEXT, files: ['.csv'] },
|
|
{ key: 'clickup', name: 'ClickUp', ...TEXT, files: ['.csv'] },
|
|
{ key: 'nullboard', name: 'Nullboard', ...TEXT, files: ['.nbx', '.json'] },
|
|
{ key: 'kanri', name: 'Kanri', ...JSON_SOURCE },
|
|
{ key: 'pivotal', name: 'Pivotal Tracker', ...TEXT, files: ['.csv'] },
|
|
{ key: 'redmine', name: 'Redmine', ...TEXT, files: ['.csv'] },
|
|
{ key: 'tasksorg', name: 'Tasks.org', ...JSON_SOURCE },
|
|
{ key: 'monday', name: 'monday.com', ...EXCEL },
|
|
{ key: 'superproductivity', name: 'Super Productivity', ...TEXT, files: ['.json'] },
|
|
{ key: 'taiga', name: 'Taiga', ...JSON_SOURCE },
|
|
{ key: 'vikunja', name: 'Vikunja', ...TEXT, files: ['.zip', '.json'], zipSend: 'zip' },
|
|
{ key: 'wrike', name: 'Wrike', ...EXCEL },
|
|
{ key: 'teamwork', name: 'Teamwork.com', ...EXCEL },
|
|
{ key: 'businessmap', name: 'Businessmap (Kanbanize)', ...EXCEL },
|
|
{ key: 'quire', name: 'Quire', ...TEXT, files: ['.csv'] },
|
|
{ key: 'notion', name: 'Notion', ...TEXT, files: ['.zip', '.csv'], zipSend: 'zip' },
|
|
{ key: 'plane', name: 'Plane', ...TEXT, files: ['.zip', '.json', '.csv', '.xlsx'], zipSend: 'zip' },
|
|
// Fetched from the service's API with the member's credentials
|
|
// (models/lib/apiImportServices.js, server/apiImport.js), or pasted as the
|
|
// JSON such a fetch returns.
|
|
{ key: 'planka', name: 'Planka', ...JSON_SOURCE, api: true },
|
|
{ key: 'mstodo', name: 'Microsoft To Do', ...JSON_SOURCE, api: true },
|
|
{ key: 'kanbanflow', name: 'KanbanFlow', ...JSON_SOURCE, api: true },
|
|
{ key: 'basecamp', name: 'Basecamp', ...JSON_SOURCE, api: true },
|
|
{ key: 'googletasks', name: 'Google Tasks', ...JSON_SOURCE, api: true },
|
|
{ key: 'googlekeep', name: 'Google Keep', ...JSON_SOURCE, api: true },
|
|
];
|
|
|
|
export const importSource = key => IMPORT_SOURCES.find(source => source.key === key) || null;
|
|
export const isGeneralized = key => (importSource(key) || {}).creator === 'generalized';
|
|
// The file chooser's accept attribute for a source.
|
|
export const acceptFor = key => ((importSource(key) || {}).files || []).join(',');
|
|
// A .zip that is one export of the tool, not a container of many files.
|
|
export const zipIsOneExport = key => Boolean((importSource(key) || {}).zipSend);
|
|
export const instructionKey = key => `import-board-instruction-${key}`;
|
|
// Whether the import page offers to fetch it from the service's API.
|
|
export const isApiImport = key => Boolean((importSource(key) || {}).api);
|