# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE from typing import TYPE_CHECKING import frappe from frappe import _ from frappe.core.doctype.permission_type.permission_type import get_doctype_ptype_map from frappe.desk.doctype.notification_log.notification_log import ( enqueue_create_notification, get_title, get_title_html, ) from frappe.desk.form.document_follow import follow_document from frappe.utils import cint if TYPE_CHECKING: from frappe.model.document import Document @frappe.whitelist() def add( doctype: str, name: str | int, user: str | None = None, read: str | bool | int = 1, write: str | bool | int = 0, submit: str | bool | int = 0, share: str | bool | int = 0, everyone: str | bool | int = 0, notify: str | bool | int = 0, **kwargs, ): """Expose function without flags to the client-side""" return add_docshare( doctype, name, user=user, read=read, write=write, submit=submit, share=share, everyone=everyone, notify=notify, **kwargs, ) def add_docshare( doctype, name, user=None, read=1, write=0, submit=0, share=0, everyone=0, flags=None, notify=0, **kwargs ): """Share the given document with a user.""" if not user: user = frappe.session.user share_perms = { # always add read, since you are adding! "read": 1, "write": cint(write), "submit": cint(submit), "share": cint(share), } custom_perms = get_doctype_ptype_map().get(doctype, []) if kwargs and custom_perms: for ptype in custom_perms: if ptype in kwargs: share_perms[ptype] = cint(kwargs.get(ptype)) if not (flags or {}).get("ignore_share_permission"): check_share_permission(doctype, name, share_perms, custom_perms) if share_name := get_share_name(doctype, name, user, everyone): doc = frappe.get_doc("DocShare", share_name) else: doc = frappe.new_doc("DocShare") doc.update({"user": user, "share_doctype": doctype, "share_name": name, "everyone": cint(everyone)}) if flags: doc.flags.update(flags) doc.update(share_perms) doc.save(ignore_permissions=True) notify_assignment(user, doctype, name, everyone, notify=notify) if frappe.get_cached_value("User", user, "follow_shared_documents"): follow_document(doctype, name, user) return doc def remove(doctype, name, user, flags=None): share_name = frappe.db.get_value("DocShare", {"user": user, "share_name": name, "share_doctype": doctype}) if share_name: frappe.delete_doc("DocShare", share_name, flags=flags) @frappe.whitelist() def set_permission( doctype: str, name: str | int, user: str | None, permission_to: str, value: str | bool | int = 1, everyone: str | bool | int = 0, ): """Expose function without flags to the client-side""" return set_docshare_permission(doctype, name, user, permission_to, value=value, everyone=everyone) def set_docshare_permission(doctype, name, user, permission_to, value=1, everyone=0, flags=None): """Set share permission.""" if not (flags or {}).get("ignore_share_permission"): permissions = {permission_to: value} if cint(value) else None check_share_permission(doctype, name, permissions) share_name = get_share_name(doctype, name, user, everyone) value = int(value) if not share_name: if value: share = add_docshare(doctype, name, user, everyone=everyone, **{permission_to: 1}, flags=flags) else: # no share found, nothing to remove share = None else: share = frappe.get_doc("DocShare", share_name) if flags: share.flags.update(flags) share.flags.ignore_permissions = True share.set(permission_to, value) if not value: # un-set higher-order permissions too if permission_to == "read": share.read = share.write = share.submit = share.share = 0 share.save() if not (share.read or share.write or share.submit or share.share): share.delete() share = None return share @frappe.whitelist() def get_users(doctype: str, name: str) -> list: """Get list of users with which this document is shared""" doc = frappe.get_lazy_doc(doctype, name) return _get_users(doc) def _get_users(doc: "Document") -> list: from frappe.permissions import has_permission if not has_permission(doc.doctype, "read", doc, print_logs=False): return [] return frappe.get_all( "DocShare", fields=["*"], filters=dict(share_doctype=doc.doctype, share_name=str(doc.name)), ) def get_shared(doctype, user=None, rights=None, *, filters=None, limit=None): """Get list of shared document names for given user and DocType. :param doctype: DocType of which shared names are queried. :param user: User for which shared names are queried. :param rights: List of rights for which the document is shared. List of `read`, `write`, `share`""" if not user: user = frappe.session.user if not rights: rights = ["read"] share_filters = [[right, "=", 1] for right in rights] share_filters += [["share_doctype", "=", doctype]] if filters: share_filters += filters or_filters = [["user", "=", user]] if user != "Guest": or_filters += [["everyone", "=", 1]] shared_docs = frappe.get_all( "DocShare", fields=["share_name"], filters=share_filters, or_filters=or_filters, order_by=None, limit_page_length=limit, ) return [doc.share_name for doc in shared_docs] def get_shared_doctypes(user=None): """Return list of doctypes in which documents are shared for the given user.""" if not user: user = frappe.session.user table = frappe.qb.DocType("DocShare") query = ( frappe.qb.from_(table) .where((table.user == user) | (table.everyone == 1)) .select(table.share_doctype) .distinct() ) return query.run(pluck=True) def get_share_name(doctype, name, user, everyone): if cint(everyone): share_name = frappe.db.get_value( "DocShare", {"everyone": 1, "share_name": name, "share_doctype": doctype} ) else: share_name = frappe.db.get_value( "DocShare", {"user": user, "share_name": name, "share_doctype": doctype} ) return share_name def check_share_permission(doctype, name, permissions=None, custom_perms=None): """Check if the user can share with other users and has the permissions they are trying to grant. :param doctype: DocType being shared :param name: Document name being shared :param permissions: Permissions that the user wants to share :param custom_perms: List of custom permission types for the doctype """ if not frappe.has_permission(doctype, ptype="share", doc=name): frappe.throw( _("No permission to {0} {1} {2}").format("share", _(doctype), name), frappe.PermissionError ) # If permissions specified, validate that the user has those permissions if not permissions: return # Validate user has the permissions they're trying to grant restricted_permissions = ["read", "write", "submit"] # Append custom permissions if custom_perms is None: custom_perms = get_doctype_ptype_map().get(doctype, []) restricted_permissions.extend(custom_perms) doc = frappe.get_doc(doctype, name) for ptype in restricted_permissions: if cint(permissions.get(ptype)) and not frappe.has_permission(doctype, ptype, doc=doc): frappe.throw( _("You cannot share `{0}` on {1} `{2}` as you do not have `{0}` permission on `{1}`").format( _(ptype), _(doctype), name ), frappe.PermissionError, ) def notify_assignment(shared_by, doctype, doc_name, everyone, notify=0): if not (shared_by and doctype and doc_name) or cint(everyone) or not cint(notify): return from frappe.utils import get_fullname title = get_title(doctype, doc_name) reference_user = get_fullname(frappe.session.user) notification_message = _("{0} shared a document {1} {2} with you").format( frappe.bold(reference_user), frappe.bold(_(doctype)), get_title_html(title) ) notification_doc = { "type": "Share", "document_type": doctype, "subject": notification_message, "document_name": doc_name, "from_user": frappe.session.user, } enqueue_create_notification(shared_by, notification_doc)