feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,115 @@
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { ModelType } from '../../BaseModel';
|
||||
import { ErrorCode } from '../../errors';
|
||||
import JoplinError from '../../JoplinError';
|
||||
import { State as ShareState } from '../../services/share/reducer';
|
||||
import ItemChange from '../ItemChange';
|
||||
import Setting from '../Setting';
|
||||
import { checkObjectHasProperties } from '@joplin/utils/object';
|
||||
import isTrashableItem from '../../services/trash/isTrashableItem';
|
||||
import isJoplinServerVariant from './isJoplinServerVariant';
|
||||
import { NoteEntity } from '../../services/database/types';
|
||||
|
||||
const logger = Logger.create('models/utils/readOnly');
|
||||
|
||||
export interface ItemSlice {
|
||||
id?: string;
|
||||
share_id: string;
|
||||
deleted_time: number;
|
||||
}
|
||||
|
||||
// This function can be called to wrap code that related to share permission read-only checks. It
|
||||
// should be fast and allows an early exit for cases that don't apply, for example if not
|
||||
// synchronising with Joplin Cloud or if not sharing any notebook.
|
||||
export const needsShareReadOnlyChecks = (itemType: ModelType, changeSource: number, shareState: ShareState, disableReadOnlyCheck = false) => {
|
||||
if (disableReadOnlyCheck) return false;
|
||||
if (!isJoplinServerVariant(Setting.value('sync.target'))) return false;
|
||||
if (changeSource === ItemChange.SOURCE_SYNC) return false;
|
||||
if (!Setting.value('sync.userId')) return false;
|
||||
if (![ModelType.Note, ModelType.Folder, ModelType.Resource].includes(itemType)) return false;
|
||||
|
||||
if (!shareState) throw new Error('Share state must be provided');
|
||||
if (!shareState.shareInvitations.length) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
export const checkIfItemsCanBeChanged = (itemType: ModelType, changeSource: number, items: ItemSlice[], shareState: ShareState) => {
|
||||
for (const item of items) {
|
||||
checkIfItemCanBeChanged(itemType, changeSource, item, shareState);
|
||||
}
|
||||
};
|
||||
|
||||
export const checkIfItemCanBeChanged = (itemType: ModelType, changeSource: number, item: ItemSlice, shareState: ShareState) => {
|
||||
if (!needsShareReadOnlyChecks(itemType, changeSource, shareState)) return;
|
||||
if (!item) return;
|
||||
|
||||
if (itemIsReadOnlySync(itemType, changeSource, item, Setting.value('sync.userId'), shareState, true)) {
|
||||
throw new JoplinError(`Cannot change or delete a read-only item: ${item.id}`, ErrorCode.IsReadOnly);
|
||||
}
|
||||
};
|
||||
|
||||
export const checkIfItemCanBeAddedToFolder = async (itemType: ModelType, Folder: typeof import('../Folder').default, changeSource: number, shareState: ShareState, parentId: string) => {
|
||||
if (needsShareReadOnlyChecks(itemType, changeSource, shareState) && parentId) {
|
||||
const parentFolder = await Folder.load(parentId, { fields: ['id', 'share_id'] });
|
||||
|
||||
if (!parentFolder) {
|
||||
// Historically it's always been possible to set the parent_id of a
|
||||
// note to a folder that does not exist - this is to support
|
||||
// synchronisation, where items are downloaded in random order. It
|
||||
// is not ideal to skip the check here, but if for some reason the
|
||||
// folder turns out to be read-only the issue will be resolved
|
||||
// during sync.
|
||||
logger.warn('checkIfItemCanBeAddedToFolder: Trying to add an item to a folder that does not exist - skipping check');
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemIsReadOnlySync(itemType, changeSource, parentFolder as ItemSlice, Setting.value('sync.userId'), shareState, true)) {
|
||||
throw new JoplinError('Cannot add an item as a child of a read-only item', ErrorCode.IsReadOnly);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// A locked note inside a share is read-only until moved out: an older client editing it there would drop the lock.
|
||||
export const noteIsLockedInShare = (note: Pick<NoteEntity, 'is_locked' | 'share_id'>) => !!note?.is_locked && !!note.share_id;
|
||||
|
||||
// Originally all these functions were there to handle share permissions - a note, folder or
|
||||
// resource that is not editable would be read-only. However this particular function now is also
|
||||
// used to tell if a note is read-only because it is in the trash.
|
||||
//
|
||||
// But this requires access to more properties, `deleted_time` in particular, which are not needed
|
||||
// for share-related checks (and does not exist on Resource objects). So this is why there's this
|
||||
// extra `sharePermissionCheckOnly` boolean to do the check for one case or the other. A bit of a
|
||||
// hack but good enough for now.
|
||||
export const itemIsReadOnlySync = (itemType: ModelType, changeSource: number, item: ItemSlice, userId: string, shareState: ShareState, sharePermissionCheckOnly = false): boolean => {
|
||||
if (!sharePermissionCheckOnly && isTrashableItem(itemType, item)) {
|
||||
checkObjectHasProperties(item, ['deleted_time']);
|
||||
}
|
||||
|
||||
// Item is in trash
|
||||
if (!sharePermissionCheckOnly && item.deleted_time) return true;
|
||||
|
||||
if (!needsShareReadOnlyChecks(itemType, changeSource, shareState)) return false;
|
||||
|
||||
checkObjectHasProperties(item, ['share_id']);
|
||||
|
||||
// Item is not shared
|
||||
if (!item.share_id) return false;
|
||||
|
||||
// Item belongs to the user
|
||||
const parentShare = shareState.shares.find(s => s.id === item.share_id);
|
||||
if (parentShare && parentShare.user?.id === userId) return false;
|
||||
|
||||
const shareUser = shareState.shareInvitations.find(si => si.share.id === item.share_id);
|
||||
|
||||
// Shouldn't happen
|
||||
if (!shareUser) return false;
|
||||
|
||||
return !shareUser.can_write;
|
||||
};
|
||||
|
||||
export const itemIsReadOnly = async (BaseItem: typeof import('../BaseItem').default, itemType: ModelType, changeSource: number, itemId: string, userId: string, shareState: ShareState): Promise<boolean> => {
|
||||
// if (!needsShareReadOnlyChecks(itemType, changeSource, shareState)) return false;
|
||||
const item: ItemSlice = await BaseItem.loadItem(itemType, itemId, { fields: ['id', 'share_id', 'deleted_time'] });
|
||||
if (!item) throw new JoplinError(`No such item: ${itemType}: ${itemId}`, ErrorCode.NotFound);
|
||||
return itemIsReadOnlySync(itemType, changeSource, item, userId, shareState);
|
||||
};
|
||||
在新工单中引用
屏蔽一个用户