diff --git a/CHANGES.rst b/CHANGES.rst index 30061e2093..423c17c820 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,6 +18,11 @@ https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst - Fix redirections to URLs with host given as IP-literal with brackets. Fixes `#1191 `_. +- Introduce the decorator ``ZPublisher.zpublish`` to explicitly + control publication by ``ZPublisher``. + For details see + `#1197 `_. + - Fix ``Content-Disposition`` filename for clients without rfc6266 support. (`#1198 `_) diff --git a/docs/zdgbook/ObjectPublishing.rst b/docs/zdgbook/ObjectPublishing.rst index 3067a70fdc..1681b1234f 100644 --- a/docs/zdgbook/ObjectPublishing.rst +++ b/docs/zdgbook/ObjectPublishing.rst @@ -130,8 +130,22 @@ Publishable Object Requirements ------------------------------- Zope has few restrictions on publishable objects. The basic rule is -that the object must have a doc string. This requirement goes for +that the object must have been marked as zpublishable. This requirement goes for methods, too. +An object or method is marked as zpublishable by decorating +its class (or a base class) or underlying function, respectively, +with the ``Zpublisher.zpublish`` decorator. +For backward compatibility, the existence of a docstring, too, +marks an object or method as zpublishable; but this will be removed in +the future. +If you decorate a method or class with ``zpublsh(False)``, +you explicitly mark it or its instances, respectively, as not +zpublishable. +If you decorate a method with ``zpublish(methods=...)`` +where the `...` is either a single request method name +or a sequence of request method names, +you specify that the object is zpublishable only for the mentioned request +methods. Another requirement is that a publishable object must not have a name that begins with an underscore. These two restrictions are designed to @@ -270,9 +284,13 @@ allow you to navigate between methods. Consider this example:: + from ZPublisher import zpublish + + @zpublish class Example: """example class""" + @zpublish def one(self): """render page one""" return """ @@ -282,6 +300,7 @@ Consider this example:: """ + @zpublish def two(self): """render page two""" return """ @@ -298,9 +317,11 @@ the URL, relative links returned by ``index_html`` won't work right. For example:: + @zpublish class Example: """example class"""" + @zpublish def index_html(self): """render default view""" return """ @@ -375,7 +396,9 @@ acquisition, you can use traversal to walk over acquired objects. Consider the the following object hierarchy:: from Acquisition import Implicit + from ZPublisher import zpublish + @zpublish class Node(Implicit): ... @@ -401,20 +424,27 @@ method that your acquire from outside your container. For example:: from Acquisition import Implicit + from ZPublisher import zpublish + @zpublish class Basket(Implicit): ... + @zpublish def number_of_items(self): """Returns the number of contained items.""" ... + @zpublish class Vegetable(Implicit): ... + @zpublish def texture(self): """Returns the texture of the vegetable.""" + @zpublish class Fruit(Implicit): ... + @zpublish def color(self): """Returns the color of the fruit.""" @@ -582,6 +612,7 @@ called from the web. Consider this function:: + @zpublish def greet(name): """Greet someone by name.""" return "Hello, %s!" % name @@ -663,6 +694,7 @@ Argument Conversion The publisher supports argument conversion. For example consider this function:: + @zpublish def one_third(number): """returns the number divided by three""" return number / 3.0 diff --git a/src/App/ApplicationManager.py b/src/App/ApplicationManager.py index 077b15be58..9794965b84 100644 --- a/src/App/ApplicationManager.py +++ b/src/App/ApplicationManager.py @@ -19,7 +19,6 @@ from urllib import parse from AccessControl.class_init import InitializeClass -from AccessControl.requestmethod import requestmethod from Acquisition import Implicit from App.CacheManager import CacheManager from App.config import getConfiguration @@ -33,6 +32,7 @@ from OFS.Traversable import Traversable from Persistence import Persistent from Products.PageTemplates.PageTemplateFile import PageTemplateFile +from ZPublisher import zpublish class FakeConnection: @@ -365,7 +365,7 @@ def db_size(self): return '%.1fM' % (s / 1048576.0) return '%.1fK' % (s / 1024.0) - @requestmethod('POST') + @zpublish(methods='POST') def manage_minimize(self, value=1, REQUEST=None): "Perform a full sweep through the cache" # XXX Add a deprecation warning about value? @@ -376,7 +376,7 @@ def manage_minimize(self, value=1, REQUEST=None): url = f'{REQUEST["URL1"]}/manage_main?manage_tabs_message={msg}' REQUEST.RESPONSE.redirect(url) - @requestmethod('POST') + @zpublish(methods='POST') def manage_pack(self, days=0, REQUEST=None): """Pack the database""" if not isinstance(days, (int, float)): diff --git a/src/App/DavLockManager.py b/src/App/DavLockManager.py index 28962f7cae..c40503602b 100644 --- a/src/App/DavLockManager.py +++ b/src/App/DavLockManager.py @@ -19,6 +19,7 @@ from App.special_dtml import DTMLFile from OFS.Lockable import wl_isLocked from OFS.SimpleItem import Item +from ZPublisher import zpublish class DavLockManager(Item, Implicit): @@ -73,6 +74,7 @@ def unlockObjects(self, paths=[]): ob.wl_clearLocks() @security.protected(webdav_manage_locks) + @zpublish def manage_unlockObjects(self, paths=[], REQUEST=None): " Management screen action to unlock objects. " if paths: diff --git a/src/App/FactoryDispatcher.py b/src/App/FactoryDispatcher.py index fb090afa9e..888bb57c21 100644 --- a/src/App/FactoryDispatcher.py +++ b/src/App/FactoryDispatcher.py @@ -25,6 +25,7 @@ from Acquisition import aq_base from ExtensionClass import Base from OFS.metaconfigure import get_registered_packages +from ZPublisher import zpublish def _product_packages(): @@ -45,6 +46,7 @@ def _product_packages(): return _packages +@zpublish class Product(Base): """Model a non-persistent product wrapper. """ @@ -68,6 +70,7 @@ def Destination(self): InitializeClass(Product) +@zpublish class ProductDispatcher(Implicit): " " # Allow access to factory dispatchers @@ -95,6 +98,7 @@ def __bobo_traverse__(self, REQUEST, name): return dispatcher.__of__(self) +@zpublish class FactoryDispatcher(Implicit): """Provide a namespace for product "methods" """ @@ -157,6 +161,7 @@ def __getattr__(self, name): _owner = UnownableOwner # Provide a replacement for manage_main that does a redirection: + @zpublish def manage_main(trueself, self, REQUEST, update_menu=0): """Implement a contents view by redirecting to the true view """ diff --git a/src/App/Management.py b/src/App/Management.py index 02b469c21e..da4e5c77d1 100644 --- a/src/App/Management.py +++ b/src/App/Management.py @@ -29,8 +29,10 @@ from App.special_dtml import DTMLFile from ExtensionClass import Base from zope.interface import implementer +from ZPublisher import zpublish +@zpublish class Tabs(Base): """Mix-in provides management folder tab support.""" @@ -68,6 +70,7 @@ def filtered_manage_options(self, REQUEST=None): manage_workspace__roles__ = ('Authenticated',) + @zpublish def manage_workspace(self, REQUEST): """Dispatch to first interface in manage_options """ @@ -181,6 +184,7 @@ def manage_page_header(self, *args, **kw): security.declarePublic('zope_copyright') # NOQA: D001 zope_copyright = DTMLFile('dtml/copyright', globals()) + @zpublish @security.public def manage_zmi_logout(self, REQUEST, RESPONSE): """Logout current user""" diff --git a/src/App/ProductContext.py b/src/App/ProductContext.py index 325f35aa60..d33a20034f 100644 --- a/src/App/ProductContext.py +++ b/src/App/ProductContext.py @@ -22,6 +22,9 @@ from App.FactoryDispatcher import FactoryDispatcher from OFS.ObjectManager import ObjectManager from zope.interface import implementedBy +from ZPublisher import zpublish +from ZPublisher import zpublish_marked +from ZPublisher import zpublish_wrap if not hasattr(Products, 'meta_types'): @@ -99,6 +102,9 @@ class will be registered. productObject = self.__prod pid = productObject.id + if instance_class is not None and not zpublish_marked(instance_class): + zpublish(instance_class) + if permissions: if isinstance(permissions, str): # You goofed it! raise TypeError( @@ -130,19 +136,21 @@ class will be registered. for method in legacy: if isinstance(method, tuple): name, method = method + mname = method.__name__ aliased = 1 else: name = method.__name__ aliased = 0 if name not in OM.__dict__: + method = zpublish_wrap(method) setattr(OM, name, method) setattr(OM, name + '__roles__', pr) if aliased: # Set the unaliased method name and its roles # to avoid security holes. XXX: All "legacy" # methods need to be eliminated. - setattr(OM, method.__name__, method) - setattr(OM, method.__name__ + '__roles__', pr) + setattr(OM, mname, method) + setattr(OM, mname + '__roles__', pr) if isinstance(initial, tuple): name, initial = initial @@ -186,7 +194,7 @@ class __FactoryDispatcher__(FactoryDispatcher): 'container_filter': container_filter },) - m[name] = initial + m[name] = zpublish_wrap(initial) m[name + '__roles__'] = pr for method in constructors[1:]: @@ -195,7 +203,7 @@ class __FactoryDispatcher__(FactoryDispatcher): else: name = os.path.split(method.__name__)[-1] if name not in productObject.__dict__: - m[name] = method + m[name] = zpublish_wrap(method) m[name + '__roles__'] = pr def getApplication(self): diff --git a/src/App/Undo.py b/src/App/Undo.py index a162858f4d..374c22ae77 100644 --- a/src/App/Undo.py +++ b/src/App/Undo.py @@ -23,6 +23,7 @@ from App.Management import Tabs from App.special_dtml import DTMLFile from DateTime.DateTime import DateTime +from ZPublisher import zpublish class UndoSupport(Tabs, Implicit): @@ -98,6 +99,7 @@ def undoable_transactions(self, first_transaction=None, return r @security.protected(undo_changes) + @zpublish def manage_undo_transactions(self, transaction_info=(), REQUEST=None): """ """ diff --git a/src/App/special_dtml.py b/src/App/special_dtml.py index 25a0104612..f1e9163452 100644 --- a/src/App/special_dtml.py +++ b/src/App/special_dtml.py @@ -33,6 +33,7 @@ from DocumentTemplate.DT_String import DTReturn from DocumentTemplate.DT_String import _marker from Shared.DC.Scripts.Bindings import Bindings +from ZPublisher import zpublish LOG = getLogger('special_dtml') @@ -46,10 +47,12 @@ class Code: pass +@zpublish class HTML(DocumentTemplate.HTML, Persistence.Persistent): "Persistent HTML Document Templates" +@zpublish class ClassicHTMLFile(DocumentTemplate.HTMLFile, MethodObject.Method): "Persistent HTML Document Templates read from files" diff --git a/src/OFS/Application.py b/src/OFS/Application.py index 92b966ce78..f591f209c0 100644 --- a/src/OFS/Application.py +++ b/src/OFS/Application.py @@ -39,6 +39,7 @@ from zExceptions import Forbidden from zExceptions import Redirect as RedirectException from zope.interface import implementer +from ZPublisher import zpublish from . import Folder from . import misc_ @@ -107,6 +108,7 @@ def Redirect(self, destination, URL1): ZopeRedirect = Redirect + @zpublish @security.protected(view_management_screens) def getZMIMainFrameTarget(self, REQUEST): """Utility method to get the right hand side ZMI frame source URL @@ -200,11 +202,13 @@ def ZopeVersion(self, major=False): return version + @zpublish def DELETE(self, REQUEST, RESPONSE): """Delete a resource object.""" self.dav__init(REQUEST, RESPONSE) raise Forbidden('This resource cannot be deleted.') + @zpublish def MOVE(self, REQUEST, RESPONSE): """Move a resource to a new location.""" self.dav__init(REQUEST, RESPONSE) diff --git a/src/OFS/CopySupport.py b/src/OFS/CopySupport.py index 1dc46d71a9..8f7d255103 100644 --- a/src/OFS/CopySupport.py +++ b/src/OFS/CopySupport.py @@ -51,6 +51,7 @@ from zope.interface import implementer from zope.lifecycleevent import ObjectCopiedEvent from zope.lifecycleevent import ObjectMovedEvent +from ZPublisher import zpublish class CopyError(Exception): @@ -91,6 +92,7 @@ def manage_CopyContainerAllItems(self, REQUEST): return [self._getOb(i) for i in REQUEST['ids']] @security.protected(delete_objects) + @zpublish def manage_cutObjects(self, ids=None, REQUEST=None): """Put a reference to the objects named in ids in the clip board""" if ids is None and REQUEST is not None: @@ -121,6 +123,7 @@ def manage_cutObjects(self, ids=None, REQUEST=None): return cp @security.protected(view_management_screens) + @zpublish def manage_copyObjects(self, ids=None, REQUEST=None, RESPONSE=None): """Put a reference to the objects named in ids in the clip board""" if ids is None and REQUEST is not None: @@ -298,6 +301,7 @@ def _pasteObjects(self, cp, cb_maxsize=0): return op, result @security.protected(view_management_screens) + @zpublish def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None): """Paste previously copied objects into the current object. @@ -334,6 +338,7 @@ def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None): manage_renameForm = DTMLFile('dtml/renameForm', globals()) @security.protected(view_management_screens) + @zpublish def manage_renameObjects(self, ids=[], new_ids=[], REQUEST=None): """Rename several sub-objects""" if len(ids) != len(new_ids): @@ -345,6 +350,7 @@ def manage_renameObjects(self, ids=[], new_ids=[], REQUEST=None): return self.manage_main(self, REQUEST) @security.protected(view_management_screens) + @zpublish def manage_renameObject(self, id, new_id, REQUEST=None): """Rename a particular sub-object. """ @@ -400,6 +406,7 @@ def manage_renameObject(self, id, new_id, REQUEST=None): return self.manage_main(self, REQUEST) @security.public + @zpublish def manage_clone(self, ob, id, REQUEST=None): """Clone an object, creating a new object with the given id. """ diff --git a/src/OFS/DTMLMethod.py b/src/OFS/DTMLMethod.py index 7a7e299ac1..682435b8b3 100644 --- a/src/OFS/DTMLMethod.py +++ b/src/OFS/DTMLMethod.py @@ -20,7 +20,6 @@ from AccessControl.Permissions import change_proxy_roles # NOQA from AccessControl.Permissions import view as View from AccessControl.Permissions import view_management_screens -from AccessControl.requestmethod import requestmethod from AccessControl.SecurityInfo import ClassSecurityInfo from AccessControl.tainted import TaintedString from Acquisition import Implicit @@ -39,6 +38,7 @@ from zExceptions import ResourceLockedError from zExceptions.TracebackSupplement import PathTracebackSupplement from zope.contenttype import guess_content_type +from ZPublisher import zpublish from ZPublisher.HTTPRequest import default_encoding from ZPublisher.Iterators import IStreamIterator @@ -51,6 +51,7 @@ class Code: pass +@zpublish class DTMLMethod( PathReprProvider, RestrictedDTML, @@ -265,6 +266,7 @@ def get_size(self): security.declareProtected(change_proxy_roles, 'manage_proxyForm') # NOQA: D001,E501 manage_proxyForm = DTMLFile('dtml/documentProxy', globals()) + @zpublish @security.protected(change_dtml_methods) def manage_edit(self, data, title, SUBMIT='Change', REQUEST=None): """ Replace contents with 'data', title with 'title'. @@ -293,6 +295,7 @@ def manage_edit(self, data, title, SUBMIT='Change', REQUEST=None): message = "Saved changes." return self.manage_main(self, REQUEST, manage_tabs_message=message) + @zpublish @security.protected(change_dtml_methods) def manage_upload(self, file='', REQUEST=None): """ Replace the contents of the document with the text in 'file'. @@ -336,8 +339,8 @@ def _validateProxy(self, roles=None): 'do not have proxy roles.\n' % ( self.__name__, user, roles)) + @zpublish(methods="POST") @security.protected(change_proxy_roles) - @requestmethod('POST') def manage_proxy(self, roles=(), REQUEST=None): """Change Proxy Roles""" user = getSecurityManager().getUser() @@ -363,6 +366,7 @@ def document_src(self, REQUEST=None, RESPONSE=None): RESPONSE.setHeader('Content-Type', 'text/plain') return self.read() + @zpublish @security.protected(change_dtml_methods) def PUT(self, REQUEST, RESPONSE): """ Handle HTTP PUT requests. diff --git a/src/OFS/FindSupport.py b/src/OFS/FindSupport.py index 842a2594c8..6ef0b6ca64 100644 --- a/src/OFS/FindSupport.py +++ b/src/OFS/FindSupport.py @@ -28,6 +28,7 @@ from ExtensionClass import Base from OFS.interfaces import IFindSupport from zope.interface import implementer +from ZPublisher import zpublish from ZPublisher.HTTPRequest import default_encoding @@ -52,6 +53,7 @@ class FindSupport(Base): }, ) + @zpublish @security.protected(view_management_screens) def ZopeFind(self, obj, obj_ids=None, obj_metatypes=None, obj_searchterm=None, obj_expr=None, @@ -69,6 +71,7 @@ def ZopeFind(self, obj, obj_ids=None, obj_metatypes=None, pre=pre, apply_func=None, apply_path='' ) + @zpublish @security.protected(view_management_screens) def ZopeFindAndApply(self, obj, obj_ids=None, obj_metatypes=None, obj_searchterm=None, obj_expr=None, diff --git a/src/OFS/Folder.py b/src/OFS/Folder.py index f577fc7b45..79fc5986af 100644 --- a/src/OFS/Folder.py +++ b/src/OFS/Folder.py @@ -27,6 +27,7 @@ from OFS.SimpleItem import PathReprProvider from webdav.Collection import Collection from zope.interface import implementer +from ZPublisher import zpublish manage_addFolderForm = DTMLFile('dtml/folderAdd', globals()) @@ -50,6 +51,7 @@ def manage_addFolder( return self.manage_main(self, REQUEST) +@zpublish @implementer(IFolder) class Folder( PathReprProvider, diff --git a/src/OFS/History.py b/src/OFS/History.py index e58faec7f5..cedc8f6ef2 100644 --- a/src/OFS/History.py +++ b/src/OFS/History.py @@ -26,6 +26,7 @@ from DateTime.DateTime import DateTime from ExtensionClass import Base from zExceptions import Redirect +from ZPublisher import zpublish view_history = 'View History' @@ -93,6 +94,7 @@ def __getitem__(self, key): return rev.__of__(self.aq_parent) + @zpublish def manage_workspace(self, REQUEST): """ We aren't real, so we delegate to that that spawned us! """ raise Redirect('/%s/manage_change_history_page' % REQUEST['URL2']) @@ -122,6 +124,7 @@ class Historical(Base): HistoryBatchSize=20, first_transaction=0, last_transaction=20) + @zpublish @security.protected(view_history) def manage_change_history(self): first = 0 @@ -146,6 +149,7 @@ def manage_change_history(self): def manage_beforeHistoryCopy(self): pass + @zpublish def manage_historyCopy(self, keys=[], RESPONSE=None, URL1=None): """ Copy a selected revision to the present """ if not keys: @@ -176,6 +180,7 @@ def manage_afterHistoryCopy(self): _manage_historyComparePage = DTMLFile( 'dtml/historyCompare', globals(), management_view='History') + @zpublish @security.protected(view_history) def manage_historyCompare(self, rev1, rev2, REQUEST, historyComparisonResults=''): @@ -186,6 +191,7 @@ def manage_historyCompare(self, rev1, rev2, REQUEST, dt1=dt1, dt2=dt2, historyComparisonResults=historyComparisonResults) + @zpublish @security.protected(view_history) def manage_historicalComparison(self, REQUEST, keys=[]): """ Compare two selected revisions """ diff --git a/src/OFS/Image.py b/src/OFS/Image.py index 7aa4972f54..4cec563ab5 100644 --- a/src/OFS/Image.py +++ b/src/OFS/Image.py @@ -48,6 +48,7 @@ from zope.lifecycleevent import ObjectCreatedEvent from zope.lifecycleevent import ObjectModifiedEvent from ZPublisher import HTTPRangeSupport +from ZPublisher import zpublish from ZPublisher.HTTPRequest import FileUpload @@ -157,6 +158,7 @@ def manage_addFile( REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_main') +@zpublish @implementer(IWriteLock, HTTPRangeSupport.HTTPRangeInterface) class File( PathReprProvider, @@ -484,6 +486,7 @@ def _should_force_download(self): # We only explicitly allow a few mimetypes, and deny the rest. return mimetype not in self.allowed_inline_mimetypes + @zpublish @security.protected(View) def index_html(self, REQUEST, RESPONSE): """ @@ -560,6 +563,7 @@ def index_html(self, REQUEST, RESPONSE): return b'' + @zpublish @security.protected(View) def view_image_or_file(self, URL1): """The default view of the contents of the File or Image.""" @@ -592,6 +596,7 @@ def _get_encoding(self): """Get the canonical encoding for ZMI.""" return ZPublisher.HTTPRequest.default_encoding + @zpublish @security.protected(change_images_and_files) def manage_edit( self, @@ -627,6 +632,7 @@ def manage_edit( return self.manage_main( self, REQUEST, manage_tabs_message=message) + @zpublish @security.protected(change_images_and_files) def manage_upload(self, file='', REQUEST=None): """ @@ -735,6 +741,7 @@ def _read_data(self, file): return (_next, size) + @zpublish @security.protected(change_images_and_files) def PUT(self, REQUEST, RESPONSE): """Handle HTTP PUT requests""" @@ -791,6 +798,7 @@ def __len__(self): data = bytes(self.data) return len(data) + @zpublish @security.protected(webdav_access) def manage_DAVget(self): """Return body for WebDAV.""" diff --git a/src/OFS/ObjectManager.py b/src/OFS/ObjectManager.py index dac5f3971d..674a5cdeed 100644 --- a/src/OFS/ObjectManager.py +++ b/src/OFS/ObjectManager.py @@ -61,6 +61,7 @@ from zope.interface.interfaces import ComponentLookupError from zope.lifecycleevent import ObjectAddedEvent from zope.lifecycleevent import ObjectRemovedEvent +from ZPublisher import zpublish from ZPublisher.HTTPResponse import make_content_disposition @@ -524,6 +525,7 @@ def superValues(self, t): manage_addProduct = ProductDispatcher() + @zpublish @security.protected(delete_objects) def manage_delObjects(self, ids=[], REQUEST=None): """Delete a subordinate object @@ -585,6 +587,7 @@ def tpValues(self): r.append(o) return r + @zpublish @security.protected(import_export_objects) def manage_exportObject( self, @@ -632,6 +635,7 @@ def manage_exportObject( security.declareProtected(import_export_objects, 'manage_importExportForm') # NOQA: D001,E501 manage_importExportForm = DTMLFile('dtml/importExport', globals()) + @zpublish @security.protected(import_export_objects) def manage_importObject(self, file, REQUEST=None, set_owner=1, suppress_events=False): diff --git a/src/OFS/OrderSupport.py b/src/OFS/OrderSupport.py index 27c3734141..517cd4b411 100644 --- a/src/OFS/OrderSupport.py +++ b/src/OFS/OrderSupport.py @@ -22,6 +22,7 @@ from zope.container.contained import notifyContainerModified from zope.interface import implementer from zope.sequencesort.ssort import sort +from ZPublisher import zpublish @implementer(IOrderedContainer) @@ -48,6 +49,7 @@ class OrderSupport: ) @security.protected(manage_properties) + @zpublish def manage_move_objects_up(self, REQUEST, ids=None, delta=1): """ Move specified sub-objects up by delta in container. """ @@ -67,6 +69,7 @@ def manage_move_objects_up(self, REQUEST, ids=None, delta=1): ) @security.protected(manage_properties) + @zpublish def manage_move_objects_down(self, REQUEST, ids=None, delta=1): """ Move specified sub-objects down by delta in container. """ @@ -86,6 +89,7 @@ def manage_move_objects_down(self, REQUEST, ids=None, delta=1): ) @security.protected(manage_properties) + @zpublish def manage_move_objects_to_top(self, REQUEST, ids=None): """ Move specified sub-objects to top of container. """ @@ -102,6 +106,7 @@ def manage_move_objects_to_top(self, REQUEST, ids=None): manage_tabs_message=message) @security.protected(manage_properties) + @zpublish def manage_move_objects_to_bottom(self, REQUEST, ids=None): """ Move specified sub-objects to bottom of container. """ @@ -118,6 +123,7 @@ def manage_move_objects_to_bottom(self, REQUEST, ids=None): manage_tabs_message=message) @security.protected(manage_properties) + @zpublish def manage_set_default_sorting(self, REQUEST, key, reverse): """ Set default sorting key and direction.""" self.setDefaultSorting(key, reverse) @@ -237,6 +243,7 @@ def setDefaultSorting(self, key, reverse): self._default_sort_key = key self._default_sort_reverse = reverse and 1 or 0 + @zpublish def manage_renameObject(self, id, new_id, REQUEST=None): """ Rename a particular sub-object without changing its position. """ diff --git a/src/OFS/PropertyManager.py b/src/OFS/PropertyManager.py index 6f6895b158..df13266f3b 100644 --- a/src/OFS/PropertyManager.py +++ b/src/OFS/PropertyManager.py @@ -27,6 +27,7 @@ from OFS.PropertySheets import vps from zExceptions import BadRequest from zope.interface import implementer +from ZPublisher import zpublish from ZPublisher.Converters import type_converters @@ -275,6 +276,7 @@ def propdict(self): dict[p['id']] = p return dict + @zpublish @security.protected(manage_properties) def manage_addProperty(self, id, value, type, REQUEST=None): """Add a new property via the web. @@ -287,6 +289,7 @@ def manage_addProperty(self, id, value, type, REQUEST=None): if REQUEST is not None: return self.manage_propertiesForm(self, REQUEST) + @zpublish @security.protected(manage_properties) def manage_editProperties(self, REQUEST): """Edit object properties via the web. @@ -312,6 +315,7 @@ def manage_editProperties(self, REQUEST): manage_tabs_message=message, ) + @zpublish @security.protected(manage_properties) def manage_changeProperties(self, REQUEST=None, **kw): """Change existing object properties. @@ -344,6 +348,7 @@ def manage_changeProperties(self, REQUEST=None, **kw): manage_tabs_message=message ) + @zpublish @security.protected(manage_properties) def manage_changePropertyTypes(self, old_ids, props, REQUEST=None): """Replace one set of properties with another. @@ -363,6 +368,7 @@ def manage_changePropertyTypes(self, old_ids, props, REQUEST=None): if REQUEST is not None: return self.manage_propertiesForm(self, REQUEST) + @zpublish @security.protected(manage_properties) def manage_delProperties(self, ids=None, REQUEST=None): """Delete one or more properties specified by 'ids'.""" diff --git a/src/OFS/PropertySheets.py b/src/OFS/PropertySheets.py index a0b0b26f5e..7db4d657ed 100644 --- a/src/OFS/PropertySheets.py +++ b/src/OFS/PropertySheets.py @@ -30,6 +30,7 @@ from Persistence import Persistent from webdav.PropertySheet import DAVPropertySheetMixin from zExceptions import BadRequest +from ZPublisher import zpublish from ZPublisher.Converters import type_converters @@ -50,6 +51,7 @@ class View(Tabs, Base): to be used as a view on an object. """ + @zpublish def manage_workspace(self, URL1, RESPONSE): '''Implement a "management" interface ''' @@ -322,11 +324,13 @@ def _propdict(self): manage = DTMLFile('dtml/properties', globals()) + @zpublish @security.protected(manage_properties) def manage_propertiesForm(self, URL1, RESPONSE): " " RESPONSE.redirect(URL1 + '/manage') + @zpublish @security.protected(manage_properties) def manage_addProperty(self, id, value, type, REQUEST=None): """Add a new property via the web. Sets a new property with @@ -337,6 +341,7 @@ def manage_addProperty(self, id, value, type, REQUEST=None): if REQUEST is not None: return self.manage(self, REQUEST) + @zpublish @security.protected(manage_properties) def manage_editProperties(self, REQUEST): """Edit object properties via the web.""" @@ -348,6 +353,7 @@ def manage_editProperties(self, REQUEST): message = 'Your changes have been saved.' return self.manage(self, REQUEST, manage_tabs_message=message) + @zpublish @security.protected(manage_properties) def manage_changeProperties(self, REQUEST=None, **kw): """Change existing object properties. @@ -372,6 +378,7 @@ def manage_changeProperties(self, REQUEST=None, **kw): message = 'Your changes have been saved.' return self.manage(self, REQUEST, manage_tabs_message=message) + @zpublish @security.protected(manage_properties) def manage_delProperties(self, ids=None, REQUEST=None): """Delete one or more properties specified by 'ids'.""" @@ -466,6 +473,7 @@ def get(self, name, default=None): return propset.__of__(self) return default + @zpublish @security.protected(manage_properties) def manage_addPropertySheet(self, id, ns, REQUEST=None): """ """ @@ -502,6 +510,7 @@ def isDeletable(self, name): return 0 return 1 + @zpublish def manage_delPropertySheets(self, ids=(), REQUEST=None): '''delete all sheets identified by *ids*.''' for id in ids: diff --git a/src/OFS/SimpleItem.py b/src/OFS/SimpleItem.py index 525dac24a1..ee0084dd34 100644 --- a/src/OFS/SimpleItem.py +++ b/src/OFS/SimpleItem.py @@ -53,6 +53,7 @@ from zExceptions import Redirect from zExceptions.ExceptionFormatter import format_exception from zope.interface import implementer +from ZPublisher import zpublish from ZPublisher.HTTPRequest import default_encoding @@ -303,6 +304,7 @@ def raise_standardErrorMessage( del self._v_eek tb = None + @zpublish def manage(self, URL1): """ """ @@ -377,6 +379,7 @@ def pretty_tb(t, v, tb, as_html=1): return tb +@zpublish @implementer(ISimpleItem) class SimpleItem( Item, diff --git a/src/OFS/owner.py b/src/OFS/owner.py index 7c0b38a045..e5491b5f68 100644 --- a/src/OFS/owner.py +++ b/src/OFS/owner.py @@ -21,13 +21,13 @@ from AccessControl.owner import ownableFilter from AccessControl.Permissions import take_ownership from AccessControl.Permissions import view_management_screens -from AccessControl.requestmethod import requestmethod from AccessControl.SecurityInfo import ClassSecurityInfo from AccessControl.SecurityManagement import getSecurityManager from AccessControl.unauthorized import Unauthorized from Acquisition import aq_get from Acquisition import aq_parent from App.special_dtml import DTMLFile +from ZPublisher import zpublish class Owned(BaseOwned): @@ -47,7 +47,7 @@ class Owned(BaseOwned): manage_owner = DTMLFile('dtml/owner', globals()) @security.protected(take_ownership) - @requestmethod('POST') + @zpublish(methods='POST') def manage_takeOwnership(self, REQUEST, RESPONSE, recursive=0): """Take ownership (responsibility) for an object. @@ -68,7 +68,7 @@ def manage_takeOwnership(self, REQUEST, RESPONSE, recursive=0): RESPONSE.redirect(REQUEST['HTTP_REFERER']) @security.protected(take_ownership) - @requestmethod('POST') + @zpublish(methods='POST') def manage_changeOwnershipType( self, explicit=1, diff --git a/src/OFS/role.py b/src/OFS/role.py index f8131308f1..343b0981e8 100644 --- a/src/OFS/role.py +++ b/src/OFS/role.py @@ -25,6 +25,7 @@ from AccessControl.rolemanager import reqattr from App.special_dtml import DTMLFile from zExceptions import BadRequest +from ZPublisher import zpublish class RoleManager(BaseRoleManager): @@ -47,7 +48,7 @@ class RoleManager(BaseRoleManager): ) @security.protected(change_permissions) - @requestmethod('POST') + @zpublish(methods='POST') def manage_role(self, role_to_manage, permissions=[], REQUEST=None): """Change the permissions given to the given role. """ @@ -64,7 +65,7 @@ def manage_role(self, role_to_manage, permissions=[], REQUEST=None): ) @security.protected(change_permissions) - @requestmethod('POST') + @zpublish(methods='POST') def manage_acquiredPermissions(self, permissions=[], REQUEST=None): """Change the permissions that acquire. """ @@ -81,7 +82,7 @@ def manage_acquiredPermissions(self, permissions=[], REQUEST=None): ) @security.protected(change_permissions) - @requestmethod('POST') + @zpublish(methods='POST') def manage_permission( self, permission_to_manage, @@ -107,12 +108,13 @@ def manage_permission( ) @security.protected(change_permissions) + @zpublish def manage_access(self, REQUEST, **kw): """Return an interface for making permissions settings.""" return self._normal_manage_access(**kw) @security.protected(change_permissions) - @requestmethod('POST') + @zpublish(methods='POST') def manage_changePermissions(self, REQUEST): """Change all permissions settings, called by management screen.""" valid_roles = self.valid_roles() @@ -158,7 +160,7 @@ def manage_changePermissions(self, REQUEST): ) @security.protected(change_permissions) - @requestmethod('POST') + @zpublish(methods='POST') def manage_addLocalRoles(self, userid, roles, REQUEST=None): """Set local roles for a user.""" BaseRoleManager.manage_addLocalRoles(self, userid, roles) @@ -167,7 +169,7 @@ def manage_addLocalRoles(self, userid, roles, REQUEST=None): return self.manage_listLocalRoles(self, REQUEST, stat=stat) @security.protected(change_permissions) - @requestmethod('POST') + @zpublish(methods='POST') def manage_setLocalRoles(self, userid, roles=[], REQUEST=None): """Set local roles for a user.""" if roles: @@ -179,7 +181,7 @@ def manage_setLocalRoles(self, userid, roles=[], REQUEST=None): return self.manage_listLocalRoles(self, REQUEST, stat=stat) @security.protected(change_permissions) - @requestmethod('POST') + @zpublish(methods='POST') def manage_delLocalRoles(self, userids, REQUEST=None): """Remove all local roles for a user.""" BaseRoleManager.manage_delLocalRoles(self, userids) @@ -188,6 +190,7 @@ def manage_delLocalRoles(self, userids, REQUEST=None): return self.manage_listLocalRoles(self, REQUEST, stat=stat) @security.protected(change_permissions) + @zpublish def manage_defined_roles(self, submit=None, REQUEST=None): """Called by management screen.""" if submit == 'Add Role': diff --git a/src/OFS/userfolder.py b/src/OFS/userfolder.py index c05efb87a9..e9a46751db 100644 --- a/src/OFS/userfolder.py +++ b/src/OFS/userfolder.py @@ -32,6 +32,7 @@ from OFS.role import RoleManager from OFS.SimpleItem import Item from zExceptions import BadRequest +from ZPublisher import zpublish class BasicUserFolder( @@ -54,7 +55,7 @@ class BasicUserFolder( ) + RoleManager.manage_options + Item.manage_options) @security.protected(ManageUsers) - @requestmethod('POST') + @zpublish(methods='POST') def userFolderAddUser(self, name, password, roles, domains, REQUEST=None, **kw): """API method for creating a new user object. Note that not all @@ -65,7 +66,7 @@ def userFolderAddUser(self, name, password, roles, domains, raise NotImplementedError @security.protected(ManageUsers) - @requestmethod('POST') + @zpublish(methods='POST') def userFolderEditUser( self, name, @@ -83,7 +84,7 @@ def userFolderEditUser( raise NotImplementedError @security.protected(ManageUsers) - @requestmethod('POST') + @zpublish(methods='POST') def userFolderDelUsers(self, names, REQUEST=None): """API method for deleting one or more user objects. Note that not all user folder implementations support deletion of user objects.""" @@ -101,6 +102,7 @@ def userFolderDelUsers(self, names, REQUEST=None): _userFolderProperties = DTMLFile('dtml/userFolderProps', globals()) + @zpublish def manage_userFolderProperties( self, REQUEST=None, @@ -115,7 +117,7 @@ def manage_userFolderProperties( management_view='Properties', ) - @requestmethod('POST') + @zpublish(methods='POST') def manage_setUserFolderProperties( self, encrypt_passwords=0, @@ -214,6 +216,7 @@ def _delUsers(self, names, REQUEST=None): return self._mainUser(self, REQUEST) @security.protected(ManageUsers) + @zpublish def manage_users(self, submit=None, REQUEST=None, RESPONSE=None): """This method handles operations on users for the web based forms of the ZMI. Application code (code that is outside of the forms diff --git a/src/Products/Five/browser/__init__.py b/src/Products/Five/browser/__init__.py index f68d54d7b8..e781b6bd51 100644 --- a/src/Products/Five/browser/__init__.py +++ b/src/Products/Five/browser/__init__.py @@ -15,8 +15,10 @@ """ from zope.publisher import browser +from ZPublisher import zpublish +@zpublish class BrowserView(browser.BrowserView): # Use an explicit __init__ to work around problems with magically inserted diff --git a/src/Products/Five/browser/tests/pages.py b/src/Products/Five/browser/tests/pages.py index a105f95810..f561bd7ab6 100644 --- a/src/Products/Five/browser/tests/pages.py +++ b/src/Products/Five/browser/tests/pages.py @@ -20,20 +20,25 @@ from OFS.SimpleItem import SimpleItem from Products.Five import BrowserView from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile +from ZPublisher import zpublish +@zpublish class SimpleView(BrowserView): """More docstring. Please Zope""" + @zpublish def eagle(self): """Docstring""" return "The eagle has landed" + @zpublish def eagle2(self): """Docstring""" return "The eagle has landed:\n%s" % self.context.absolute_url() + @zpublish def mouse(self): """Docstring""" return "The mouse has been eaten by the eagle" @@ -105,16 +110,19 @@ class ProtectedView: security = ClassSecurityInfo() + @zpublish @security.public def public_method(self): """Docstring""" return 'PUBLIC' + @zpublish @security.protected('View') def protected_method(self): """Docstring""" return 'PROTECTED' + @zpublish @security.private def private_method(self): """Docstring""" @@ -133,6 +141,7 @@ def meat(): class CheeseburgerView(BrowserView): """View those `meat` method gets allowed via `IHamburger`.""" + @zpublish def meat(self): """Make meat publically available via a docstring.""" return 'yummi' diff --git a/src/Products/Five/tests/testing/fancycontent.py b/src/Products/Five/tests/testing/fancycontent.py index dc1971960b..93271d76af 100644 --- a/src/Products/Five/tests/testing/fancycontent.py +++ b/src/Products/Five/tests/testing/fancycontent.py @@ -20,12 +20,14 @@ from OFS.SimpleItem import SimpleItem from zope.interface import Interface from zope.interface import implementer +from ZPublisher import zpublish class IFancyContent(Interface): pass +@zpublish class FancyAttribute(Explicit): """Doc test fanatics""" @@ -34,6 +36,7 @@ def __init__(self, name): security = ClassSecurityInfo() + @zpublish @security.public def index_html(self, REQUEST): """Doc test fanatics""" diff --git a/src/Products/Five/tests/testing/simplecontent.py b/src/Products/Five/tests/testing/simplecontent.py index 6845656fbd..3e4b6803ae 100644 --- a/src/Products/Five/tests/testing/simplecontent.py +++ b/src/Products/Five/tests/testing/simplecontent.py @@ -19,6 +19,7 @@ from OFS.SimpleItem import SimpleItem from zope.interface import Interface from zope.interface import implementer +from ZPublisher import zpublish class ISimpleContent(Interface): @@ -77,6 +78,7 @@ class IndexSimpleContent(SimpleItem): meta_type = 'Five IndexSimpleContent' + @zpublish def index_html(self, *args, **kw): """ """ return "Default index_html called" diff --git a/src/Products/PageTemplates/ZopePageTemplate.py b/src/Products/PageTemplates/ZopePageTemplate.py index bbd13e0ac6..ab9264c89b 100644 --- a/src/Products/PageTemplates/ZopePageTemplate.py +++ b/src/Products/PageTemplates/ZopePageTemplate.py @@ -39,6 +39,7 @@ from Shared.DC.Scripts.Script import Script from Shared.DC.Scripts.Signature import FuncCode from zExceptions import ResourceLockedError +from ZPublisher import zpublish preferred_encodings = ['utf-8', 'iso-8859-15'] @@ -66,6 +67,7 @@ def __call__(self, REQUEST, RESPONSE): InitializeClass(Src) +@zpublish class ZopePageTemplate(Script, PageTemplate, Historical, Cacheable, Traversable, PropertyManager): "Zope wrapper for Page Template using TAL, TALES, and METAL" @@ -145,6 +147,7 @@ def pt_edit(self, text, content_type, keep_output_encoding=False): source_dot_xml = Src() + @zpublish @security.protected(change_page_templates) def pt_editAction(self, REQUEST, title, text, content_type, expand=0): """Change the title and document.""" @@ -178,6 +181,7 @@ def _setPropValue(self, id, value): PropertyManager._setPropValue(self, id, value) self.ZCacheable_invalidate() + @zpublish @security.protected(change_page_templates) def pt_upload(self, REQUEST, file='', encoding='utf-8'): """Replace the document with the text in file.""" @@ -344,6 +348,7 @@ def pt_render(self, source=False, extra_context={}): assert isinstance(result, str) return result + @zpublish @security.protected(change_page_templates) def PUT(self, REQUEST, RESPONSE): """ Handle HTTP PUT requests """ diff --git a/src/Products/SiteAccess/VirtualHostMonster.py b/src/Products/SiteAccess/VirtualHostMonster.py index b5eedc0496..4732a98f42 100644 --- a/src/Products/SiteAccess/VirtualHostMonster.py +++ b/src/Products/SiteAccess/VirtualHostMonster.py @@ -11,6 +11,7 @@ from Persistence import Persistent from zExceptions import BadRequest from zope.publisher.http import splitport +from ZPublisher import zpublish from ZPublisher.BaseRequest import quote from ZPublisher.BeforeTraverse import NameCaller from ZPublisher.BeforeTraverse import queryBeforeTraverse @@ -18,6 +19,7 @@ from ZPublisher.BeforeTraverse import unregisterBeforeTraverse +@zpublish class VirtualHostMonster(Persistent, Item, Implicit): """Provide a simple drop-in solution for virtual hosting. """ @@ -46,6 +48,7 @@ class VirtualHostMonster(Persistent, Item, Implicit): security.declareProtected('Add Site Roots', 'manage_edit') # NOQA: D001 manage_edit = DTMLFile('www/manage_edit', globals()) + @zpublish @security.protected('Add Site Roots') def set_map(self, map_text, RESPONSE=None): "Set domain to path mappings." diff --git a/src/Shared/DC/Scripts/Bindings.py b/src/Shared/DC/Scripts/Bindings.py index faa782d4ba..b964818e56 100644 --- a/src/Shared/DC/Scripts/Bindings.py +++ b/src/Shared/DC/Scripts/Bindings.py @@ -24,6 +24,7 @@ from Acquisition import aq_inner from Acquisition import aq_parent from zope.component import queryMultiAdapter as qma +from ZPublisher import zpublish defaultBindings = {'name_context': 'context', @@ -207,6 +208,7 @@ def __you_lose(self): __str__ = __call__ = index_html = __you_lose +@zpublish class Bindings: security = ClassSecurityInfo() diff --git a/src/Shared/DC/Scripts/BindingsUI.py b/src/Shared/DC/Scripts/BindingsUI.py index a3a243e760..b0256b60d4 100644 --- a/src/Shared/DC/Scripts/BindingsUI.py +++ b/src/Shared/DC/Scripts/BindingsUI.py @@ -16,6 +16,7 @@ from AccessControl.SecurityInfo import ClassSecurityInfo from App.special_dtml import DTMLFile from Shared.DC.Scripts.Bindings import Bindings +from ZPublisher import zpublish class BindingsUI(Bindings): @@ -30,6 +31,7 @@ class BindingsUI(Bindings): 'ZBindingsHTML_editForm') ZBindingsHTML_editForm = DTMLFile('dtml/scriptBindings', globals()) + @zpublish @security.protected('Change bindings') def ZBindingsHTML_editAction(self, REQUEST): '''Changes binding names. diff --git a/src/Testing/tests/test_testbrowser.py b/src/Testing/tests/test_testbrowser.py index 7165291454..78733250bd 100644 --- a/src/Testing/tests/test_testbrowser.py +++ b/src/Testing/tests/test_testbrowser.py @@ -25,10 +25,12 @@ from Testing.ZopeTestCase import user_name from Testing.ZopeTestCase import user_password from zExceptions import NotFound +from ZPublisher import zpublish from ZPublisher.httpexceptions import HTTPExceptionHandler from ZPublisher.WSGIPublisher import publish_module +@zpublish class CookieStub(Item): """This is a cookie stub.""" @@ -37,6 +39,7 @@ def __call__(self, REQUEST): return 'Stub' +@zpublish class ExceptionStub(Item): """This is a stub, raising an exception.""" @@ -44,6 +47,7 @@ def __call__(self, REQUEST): raise ValueError('dummy') +@zpublish class RedirectStub(Item): """This is a stub, causing a redirect.""" diff --git a/src/ZPublisher/BaseRequest.py b/src/ZPublisher/BaseRequest.py index 7063cd0153..ab8382cbbd 100644 --- a/src/ZPublisher/BaseRequest.py +++ b/src/ZPublisher/BaseRequest.py @@ -1,6 +1,6 @@ ############################################################################## # -# Copyright (c) 2002 Zope Foundation and Contributors. +# Copyright (c) 2002-2024 Zope Foundation and Contributors. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. @@ -14,6 +14,8 @@ """ import types +import warnings +from os import environ from urllib.parse import quote as urllib_quote from AccessControl.ZopeSecurityPolicy import getRoles @@ -35,6 +37,7 @@ from zope.publisher.interfaces.browser import IBrowserPublisher from zope.traversing.namespace import namespaceLookup from zope.traversing.namespace import nsParse +from ZPublisher import zpublish_mark from ZPublisher.Converters import type_converters from ZPublisher.interfaces import UseTraversalDefault from ZPublisher.xmlrpc import is_xmlrpc_response @@ -136,22 +139,7 @@ def publishTraverse(self, request, name): except TypeError: # unsubscriptable raise KeyError(name) - # Ensure that the object has a docstring, or that the parent - # object has a pseudo-docstring for the object. Objects that - # have an empty or missing docstring are not published. - doc = getattr(subobject, '__doc__', None) - if not doc: - raise Forbidden( - "The object at %s has an empty or missing " - "docstring. Objects must have a docstring to be " - "published." % URL - ) - - # Check that built-in types aren't publishable. - if not typeCheck(subobject): - raise Forbidden( - "The object at %s is not publishable." % URL) - + self.request.ensure_publishable(subobject) return subobject def browserDefault(self, request): @@ -502,6 +490,7 @@ def traverse(self, path, response=None, validated_hook=None): self.roles = getRoles( object, '__call__', object.__call__, self.roles) + self.ensure_publishable(object.__call__, True) if request._hacked_path: i = URL.rfind('/') if i > 0: @@ -685,6 +674,63 @@ def _hold(self, object): if self._held is not None: self._held = self._held + (object, ) + def ensure_publishable(self, obj, for_call=False): + """raise ``Forbidden`` unless *obj* is publishable. + + *for_call* tells us whether we are called for the ``__call__`` + method. In general, its publishablity is determined by + its ``__self__`` but it might have more restrictive prescriptions. + """ + url, default = self["URL"], None + if for_call: + # We are called to check the publication + # of the ``__call__`` method. + # Usually, its publication indication comes from its + # ``__self__`` and this has already been checked. + # It can however carry a stricter publication indication + # which we want to check here. + # We achieve this by changing *default* from + # ``None`` to ``True``. In this way, we get the publication + # indication of ``__call__`` if it carries one + # or ``True`` otherwise which in this case + # indicates "already checked". + url += "[__call__]" + default = True + publishable = zpublish_mark(obj, default) + # ``publishable`` is either ``None``, ``True``, ``False`` or + # a tuple of allowed request methods. + if publishable is True: # explicitely marked as publishable + return + elif publishable is False: # explicitely marked as not publishable + raise Forbidden( + f"The object at {url} is marked as not publishable") + elif publishable is not None: + # a tuple of allowed request methods + request_method = (getattr(self, "environ", None) + and self.environ.get("REQUEST_METHOD")) + if (request_method is None # noqa: E271 + or request_method.upper() not in publishable): + raise Forbidden( + f"The object at {url} does not support " + f"{request_method} requests") + return + # ``publishable`` is ``None`` + + # Check that built-in types aren't publishable. + if not typeCheck(obj): + raise Forbidden( + "The object at %s is not publishable." % url) + # Ensure that the object has a docstring + doc = getattr(obj, '__doc__', None) + if not doc: + raise Forbidden( + f"The object at {url} has an empty or missing " + "docstring. Objects must either be marked via " + "to `ZPublisher.zpublish` decorator or have a docstring to be " + "published.") + if deprecate_docstrings: + warnings.warn(DocstringWarning(obj, url)) + def exec_callables(callables): result = None @@ -776,3 +822,66 @@ def old_validation(groups, request, auth, def typeCheck(obj, deny=itypes): # Return true if its ok to publish the type, false otherwise. return deny.get(type(obj), 1) + + +deprecate_docstrings = environ.get("ZPUBLISHER_DEPRECATE_DOCSTRINGS") + + +class DocstringWarning(DeprecationWarning): + def tag(self): + import inspect as i + + def lineno(o, m=False): + """try to determine where *o* has been defined. + + *o* is either a function or a class. + """ + try: + _, lineno = i.getsourcelines(o) + except (OSError, TypeError): + return "" + return f"[{o.__module__}:{lineno}]" if m else f" at line {lineno}" + + obj, url = self.args + desc = None + if i.ismethod(obj): + f = i.unwrap(obj.__func__) + c = obj.__self__.__class__ + desc = f"'{c.__module__}.{c.__qualname__}' " \ + f"method '{obj.__qualname__}'{lineno(f, 1)}" + elif i.isfunction(obj): + f = i.unwrap(obj) + desc = f"function '{f.__module__}.{f.__qualname__}'" \ + f"{lineno(f)}" + else: + try: + cls_doc = "__doc__" not in obj.__dict__ + except AttributeError: + cls_doc = True + if cls_doc: + c = obj.__class__ + desc = f"'{c.__module__}.{c.__qualname__}'{lineno(c)}" + if desc is None: + desc = f"object at '{url}'" + return desc + + def __str__(self): + return (f"{self.tag()} uses deprecated docstring " + "publication control. Use the `ZPublisher.zpublish` decorator " + "instead") + + +if deprecate_docstrings: + # look whether there is already a ``DocstringWarning`` filter + for f in warnings.filters: + if f[2] is DocstringWarning: + break + else: + # provide a ``DocstringWarning`` filter + # if ``deprecate_docstrings`` specifies a sensefull action + # use it, otherwise ``"default"``. + warn_action = deprecate_docstrings \ + if deprecate_docstrings \ + in ("default", "error", "ignore", "always") \ + else "default" + warnings.filterwarnings(warn_action, category=DocstringWarning) diff --git a/src/ZPublisher/__init__.py b/src/ZPublisher/__init__.py index cf4b87548e..51ee1f880a 100644 --- a/src/ZPublisher/__init__.py +++ b/src/ZPublisher/__init__.py @@ -1,6 +1,6 @@ ############################################################################## # -# Copyright (c) 2002 Zope Foundation and Contributors. +# Copyright (c) 2002-2024 Zope Foundation and Contributors. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. @@ -11,6 +11,13 @@ # ############################################################################## +from functools import wraps +from inspect import Parameter +from inspect import Signature +from inspect import signature +from itertools import chain +from types import FunctionType + class Retry(Exception): """Raise this to retry a request @@ -29,3 +36,119 @@ def reraise(self): raise v.with_traceback(tb) finally: tb = None + + +_ZPUBLISH_ATTR = "__zpublishable__" + + +def zpublish(publish=True, *, methods=None): + """decorator signaling design for/not for publication. + + Usage: + + @zpublish + def f(...): ... + + @zpublish(True) + def f(...): ... + + ``f`` is designed to be published by ``ZPublisher``. + + @zpublish(False) + def f(...): ... + ``ZPublisher`` should not publish ``f`` + + @zpublish(methods="METHOD") + def f(...): + ``ZPublisher`` should publish ``f`` for request method *METHOD* + + zpublish(methods=("M1", "M2", ...)) + def f(...): + ``ZPublisher`` should publish ``f`` for all + request methods mentioned in *methods*. + + + @zpublish... + class C: ... + instances of ``C`` can/can not be published by ``ZPublisher``. + + + ``zpublish(f)`` is equivalent to ``zpublish(True)(f)`` if + ``f`` is not a boolean. + """ + if not isinstance(publish, bool): + return zpublish(True)(publish) + + if methods is not None: + assert publish + publish = ((methods.upper(),) if isinstance(methods, str) + else tuple(m.upper() for m in methods) if methods + else False) + + def wrap(f): + # *publish* is either ``True``, ``False`` or a tuple + # of allowed request methods + setattr(f, _ZPUBLISH_ATTR, publish) + return f + + return wrap + + +def zpublish_mark(obj, default=None): + """the publication indication effective at *obj* or *default*. + + For an instance, the indication usually comes from its class + or a base class; a function/method typically carries the + indication itself. + + The publication indication is either ``True`` (publication allowed), + ``False`` (publication disallowed) or a tuple + of request method names for which publication is allowed. + """ + return getattr(obj, _ZPUBLISH_ATTR, default) + + +def zpublish_marked(obj): + """true if a publication indication is effective at *obj*.""" + return zpublish_mark(obj) is not None + + +def zpublish_wrap(callable, *, conditional=True, publish=True, methods=None): + """wrap *callable* to provide a publication indication. + + Return *callable* unchanged if *conditional* and a publication indication + is already effective at *callable*; + otherwise, return a signature preserving wrapper + with publication control given by *publish* and *methods*. + """ + if conditional and zpublish_marked(callable): + return callable + + @zpublish(publish, methods=methods) + @wraps(callable) + def wrapper(*args, **kw): + return callable(*args, **kw) + # Signature preservation is particularly important for ``mapply``. + # It allows an instance to specify the signature to be used for + # its ``__call__`` method via attributes ``__code__`` and + # ``__defaults__``. + # We must respect such specifications + cls = callable.__class__ + if isinstance(getattr(cls, "__call__", None), FunctionType) \ + and getattr(callable, "__code__", cls) is not cls \ + and getattr(callable, "__defaults__", cls) is not cls: + # Signature specification via ``__code__`` and ``__defaults__``. + code = callable.__code__ + varnames = code.co_varnames + argcount = code.co_argcount + defaults = callable.__defaults__ or () + pos = argcount - len(defaults) + sig = Signature(tuple( + Parameter(z[0], Parameter.POSITIONAL_OR_KEYWORD, default=z[1]) + for z in chain( + ((n, Parameter.empty) for n in varnames[:pos]), + zip(varnames[pos:], defaults)))) + else: + sig = signature(callable) + wrapper.__signature__ = sig + return wrapper diff --git a/src/ZPublisher/tests/docstring.py b/src/ZPublisher/tests/docstring.py new file mode 100644 index 0000000000..bd9bd4a20b --- /dev/null +++ b/src/ZPublisher/tests/docstring.py @@ -0,0 +1,13 @@ +"""Resources for ``testBaseRequest.TestDocstringWarning``. + +The tests there depend on line numbers in this module. +""" + + +def f(): + "f" + + +class C: + "C" + g = f diff --git a/src/ZPublisher/tests/testBaseRequest.py b/src/ZPublisher/tests/testBaseRequest.py index 32fdbfcb3f..a4b7665bf8 100644 --- a/src/ZPublisher/tests/testBaseRequest.py +++ b/src/ZPublisher/tests/testBaseRequest.py @@ -1,9 +1,14 @@ import unittest +from zExceptions import Forbidden from zExceptions import NotFound from zope.interface import implementer from zope.publisher.interfaces import IPublishTraverse from zope.publisher.interfaces import NotFound as ztkNotFound +from ZPublisher import zpublish +from ZPublisher.BaseRequest import DocstringWarning + +from . import docstring @implementer(IPublishTraverse) @@ -52,41 +57,61 @@ def notFoundError(self, name): } return self._getTargetClass()(environment) - def _makeBasicObjectClass(self): + @staticmethod + def _use_docstring(ud): + if ud is not None: + return ud + from .. import BaseRequest + return not BaseRequest.deprecate_docstrings + + def _makeBasicObjectClass(self, use_docstring=None): from Acquisition import Implicit class DummyObjectBasic(Implicit): - """Dummy class with docstring.""" def _setObject(self, id, object): setattr(self, id, object) return getattr(self, id) def view(self): - """Attribute with docstring.""" + # publishable return 'view content' def noview(self): - # Attribute without docstring. + # not publishable return 'unpublishable' def __contains__(self, name): return False + if self._use_docstring(use_docstring): + DummyObjectBasic.__doc__ = "with docstring" + DummyObjectBasic.view.__doc__ = "with docstring" + else: + zpublish(DummyObjectBasic) + zpublish(DummyObjectBasic.view) + return DummyObjectBasic - def _makeBasicObject(self): - return self._makeBasicObjectClass()() + def _makeBasicObject(self, use_docstring=None): + return self._makeBasicObjectClass(use_docstring)() - def _makeObjectWithDefault(self): + def _makeObjectWithDefault(self, use_docstring=None): - class DummyObjectWithDefault(self._makeBasicObjectClass()): - """Dummy class with docstring.""" + base = self._makeBasicObjectClass(use_docstring) + class DummyObjectWithDefault(base): def index_html(self): - """Attribute with docstring.""" + # publishable return 'index_html content' + if self._use_docstring(use_docstring): + DummyObjectWithDefault.__doc__ = "with docstring" + DummyObjectWithDefault.index_html.__doc__ = "with docstring" + else: + zpublish(DummyObjectWithDefault) + zpublish(DummyObjectWithDefault.index_html) + return DummyObjectWithDefault() def _makeObjectWithDefaultNone(self): @@ -127,6 +152,7 @@ def __browser_default__(self, REQUEST): def _makeObjectWithBBT(self): from ZPublisher.interfaces import UseTraversalDefault + @zpublish class _DummyResult: ''' ''' def __init__(self, tag): @@ -162,11 +188,10 @@ def __bobo_traverse__(self, REQUEST, name): raise AttributeError(name) return DummyObjectWithBDBBT() - def _makeObjectWithEmptyDocstring(self): + def _makeObjectWithEmptyDocstring(self, use_docstring=None): from Acquisition import Implicit class DummyObjectWithEmptyDocstring(Implicit): - "" def view(self): """Attribute with docstring.""" return 'view content' @@ -174,6 +199,10 @@ def view(self): def noview(self): # Attribute without docstring. return 'unpublishable' + if self._use_docstring(use_docstring): + DummyObjectWithEmptyDocstring.__doc__ = "" + else: + zpublish(False)(DummyObjectWithEmptyDocstring) return DummyObjectWithEmptyDocstring() @@ -183,9 +212,10 @@ def _getTargetClass(self): from ZPublisher.BaseRequest import BaseRequest return BaseRequest - def _makeRootAndFolder(self): - root = self._makeBasicObject() - folder = root._setObject('folder', self._makeBasicObject()) + def _makeRootAndFolder(self, use_docstring=None): + root = self._makeBasicObject(use_docstring) + folder = root._setObject('folder', + self._makeBasicObject(use_docstring)) return root, folder def test_no_docstring_on_instance(self): @@ -343,9 +373,9 @@ def test_traverse_slash(self): self.assertEqual(r.URL, '/index_html') self.assertEqual(r.response.base, '') - def test_traverse_attribute_with_docstring(self): + def test_traverse_attribute_with_docstring(self, use_docstring=None): root, folder = self._makeRootAndFolder() - folder._setObject('objBasic', self._makeBasicObject()) + folder._setObject('objBasic', self._makeBasicObject(use_docstring)) r = self._makeOne(root) r.traverse('folder/objBasic/view') self.assertEqual(r.URL, '/folder/objBasic/view') @@ -387,6 +417,80 @@ def test_traverse_attribute_and_class_without_docstring(self): self.assertRaises(NotFound, r.traverse, 'folder/objWithoutDocstring/noview') + def test_traverse_attribute_with_zpublish(self): + root, folder = self._makeRootAndFolder(False) + folder._setObject('objBasic', self._makeBasicObject(False)) + r = self._makeOne(root) + r.traverse('folder/objBasic/view') + self.assertEqual(r.URL, '/folder/objBasic/view') + self.assertEqual(r.response.base, '') + + def test_traverse_attribute_without_zpublish(self): + root, folder = self._makeRootAndFolder(False) + folder._setObject('objBasic', self._makeBasicObject(False)) + r = self._makeOne(root) + self.assertRaises(NotFound, r.traverse, 'folder/objBasic/noview') + + def test_traverse_acquired_attribute_without_zpublish(self): + root, folder = self._makeRootAndFolder(False) + root._setObject('objBasic', + self._makeObjectWithEmptyDocstring()) + r = self._makeOne(root) + self.assertRaises(NotFound, r.traverse, 'folder/objBasic') + + def test_traverse_class_without_zpublish(self): + root, folder = self._makeRootAndFolder(False) + folder._setObject('objWithoutDocstring', + self._makeObjectWithEmptyDocstring(False)) + r = self._makeOne(root) + self.assertRaises(NotFound, r.traverse, 'folder/objWithoutDocstring') + + def test_traverse_attribute_of_class_without_zpublish(self): + root, folder = self._makeRootAndFolder(False) + folder._setObject('objWithoutDocstring', + self._makeObjectWithEmptyDocstring(False)) + r = self._makeOne(root) + self.assertRaises(NotFound, r.traverse, + 'folder/objWithoutDocstring/view') + + def test_traverse_attribute_and_class_without_zpublish(self): + root, folder = self._makeRootAndFolder(False) + r = self._makeOne(root) + folder._setObject('objWithoutDocstring', + self._makeObjectWithEmptyDocstring(False)) + self.assertRaises(NotFound, r.traverse, + 'folder/objWithoutDocstring/noview') + + def test_docstring_deprecation(self): + from ZPublisher import BaseRequest + deprecate = BaseRequest.deprecate_docstrings + try: + BaseRequest.deprecate_docstrings = "1" + with self.assertWarns(BaseRequest.DocstringWarning): + self.test_traverse_attribute_with_docstring(True) + finally: + BaseRequest.deprecate_docstrings = deprecate + + def test_zpublish___call__(self): + root, folder = self._makeRootAndFolder(False) + + @zpublish(methods="POST") + def __call__(self): + pass + + folder.__class__.__call__ = __call__ + # no request method + r = self._makeOne(root) + self.assertRaises(Forbidden, r.traverse, 'folder') + # wrong request method + r = self._makeOne(root) + r.environ = dict(REQUEST_METHOD="get") + self.assertRaises(Forbidden, r.traverse, 'folder') + # correct request method + r = self._makeOne(root) + r.environ = dict(REQUEST_METHOD="post") + r.traverse('folder') + def test_traverse_simple_string(self): root, folder = self._makeRootAndFolder() folder.simpleString = 'foo' @@ -541,6 +645,11 @@ def methonly(self): """doc""" return 'methonly on %s' % self.name + if not self._use_docstring(None): + zpublish(DummyObjectZ3WithAttr) + zpublish(DummyObjectZ3WithAttr.meth) + zpublish(DummyObjectZ3WithAttr.methonly) + return DummyObjectZ3WithAttr(name) def setUp(self): @@ -779,3 +888,37 @@ def test__str__returns_native_string(self): root, folder = self._makeRootAndFolder() r = self._makeOne(root) self.assertIsInstance(str(r), str) + + +class TestDocstringWarning(unittest.TestCase): + def test_method(self): + c = docstring.C() + ds = DocstringWarning(c.g, "URL") + self.assertEqual(ds.tag(), + "'ZPublisher.tests.docstring.C' method " + "'f'[ZPublisher.tests.docstring:7]") + + def test_function(self): + f = docstring.f + ds = DocstringWarning(f, "URL") + self.assertEqual(ds.tag(), + "function 'ZPublisher.tests.docstring.f' at line 7") + + def test_instance_with_own_docstring(self): + c = docstring.C() + c.__doc__ = "c" + ds = DocstringWarning(c, "URL") + self.assertEqual(ds.tag(), "object at 'URL'") + + def test_instance_with_inherited_docstring(self): + c = docstring.C() + ds = DocstringWarning(c, "URL") + self.assertEqual(ds.tag(), "'ZPublisher.tests.docstring.C' at line 11") + + def test__str__(self): + ds = DocstringWarning("special", "URL") + self.assertEqual( + str(ds), + "'builtins.str' uses deprecated docstring " + "publication control. Use the `ZPublisher.zpublish` decorator " + "instead") diff --git a/src/ZPublisher/tests/testBeforeTraverse.py b/src/ZPublisher/tests/testBeforeTraverse.py index 1e40e7f009..3796fd6023 100644 --- a/src/ZPublisher/tests/testBeforeTraverse.py +++ b/src/ZPublisher/tests/testBeforeTraverse.py @@ -1,6 +1,7 @@ import doctest from Acquisition import Implicit +from ZPublisher import zpublish from ZPublisher.BaseRequest import BaseRequest from ZPublisher.HTTPResponse import HTTPResponse @@ -18,6 +19,7 @@ def makeBaseRequest(root): return BaseRequest(environment) +@zpublish class DummyObjectBasic(Implicit): """ Dummy class with docstring. """ diff --git a/src/ZPublisher/tests/testPostTraversal.py b/src/ZPublisher/tests/testPostTraversal.py index b6c5c806f7..6a43274a47 100644 --- a/src/ZPublisher/tests/testPostTraversal.py +++ b/src/ZPublisher/tests/testPostTraversal.py @@ -2,6 +2,7 @@ import Zope2 from Acquisition import Implicit +from ZPublisher import zpublish from ZPublisher.BaseRequest import BaseRequest from ZPublisher.HTTPResponse import HTTPResponse @@ -34,6 +35,7 @@ def pt_chain_test(request, string): request.set('a', request.get('a', '') + string) +@zpublish class DummyObjectBasic(Implicit): """ Dummy class with docstring. """ @@ -42,12 +44,14 @@ def _setObject(self, id, object): setattr(self, id, object) return getattr(self, id) + @zpublish def view(self): """ Attribute with docstring. """ return 'view content' +@zpublish class DummyObjectWithPTHook(DummyObjectBasic): """ Dummy class with docstring. """ diff --git a/src/ZPublisher/tests/test_pubevents.py b/src/ZPublisher/tests/test_pubevents.py index fad2f32eca..d1e4a95551 100644 --- a/src/ZPublisher/tests/test_pubevents.py +++ b/src/ZPublisher/tests/test_pubevents.py @@ -16,6 +16,7 @@ from zope.interface.verify import verifyObject from zope.publisher.interfaces import INotFound from zope.publisher.interfaces.browser import IDefaultBrowserLayer +from ZPublisher import zpublish from ZPublisher.BaseRequest import BaseRequest from ZPublisher.HTTPRequest import WSGIRequest from ZPublisher.HTTPResponse import WSGIResponse @@ -279,6 +280,7 @@ def test_BeforeAbort_and_Failure_events_are_called_after_exc_view(self): def test_exception_views_and_event_handlers_get_upgraded_exceptions(self): self.expected_exception_type = zExceptions.HTTPVersionNotSupported + @zpublish def raiser(*args, **kwargs): "Allow publishing" class HTTPVersionNotSupported(Exception): diff --git a/src/ZPublisher/tests/test_zpublish.py b/src/ZPublisher/tests/test_zpublish.py new file mode 100644 index 0000000000..8e779f0b18 --- /dev/null +++ b/src/ZPublisher/tests/test_zpublish.py @@ -0,0 +1,104 @@ +############################################################################## +# +# Copyright (c) 2024 Zope Foundation and Contributors. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""``zpublish`` related tests.""" + +from inspect import signature +from unittest import TestCase + +from Shared.DC.Scripts.Signature import FuncCode + +from .. import zpublish +from .. import zpublish_mark +from .. import zpublish_marked +from .. import zpublish_wrap + + +class ZpublishTests(TestCase): + def test_zpublish_true(self): + @zpublish + def f(): + pass + + self.assertIs(zpublish_mark(f), True) + self.assertTrue(zpublish_marked(f)) + + def test_zpublish_false(self): + @zpublish(False) + def f(): + pass + + self.assertIs(zpublish_mark(f), False) + self.assertTrue(zpublish_marked(f)) + + def test_zpublish_method(self): + @zpublish(methods="method") + def f(): + pass + + self.assertEqual(zpublish_mark(f), ("METHOD",)) + self.assertTrue(zpublish_marked(f)) + + def test_zpublish_methods(self): + @zpublish(methods="m1 m2".split()) + def f(): + pass + + self.assertEqual(zpublish_mark(f), ("M1", "M2")) + self.assertTrue(zpublish_marked(f)) + + def test_zpublish_mark(self): + def f(): + pass + + self.assertIsNone(zpublish_mark(f)) + self.assertIs(zpublish_mark(f, True), True) + zpublish(f) + self.assertIs(zpublish_mark(f), True) + + def test_zpublish_marked(self): + def f(): + pass + + self.assertFalse(zpublish_marked(f)) + zpublish(f) + self.assertTrue(zpublish_marked(f)) + + def test_zpublish_wrap(self): + def f(): + pass + + self.assertFalse(zpublish_marked(f)) + wrapper = zpublish_wrap(f) + self.assertFalse(zpublish_marked(f)) + self.assertIs(zpublish_mark(wrapper), True) + self.assertEqual(signature(wrapper), signature(f)) + self.assertIs(wrapper, zpublish_wrap(wrapper)) + wrapper2 = zpublish_wrap(wrapper, conditional=False, methods="put") + self.assertIsNot(wrapper2, wrapper) + self.assertEqual(zpublish_mark(wrapper2), ("PUT",)) + + # test ``mapply`` signature + + class WithMapplySignature: + __code__ = FuncCode(("a", "b", "c"), 2) + __defaults__ = None + + def __call__(self, *args, **kw): + pass + + f = WithMapplySignature() + wrapper = zpublish_wrap(f) + self.assertEqual(str(wrapper.__signature__), "(a, b)") + WithMapplySignature.__defaults__ = 2, + wrapper = zpublish_wrap(f) + self.assertEqual(str(wrapper.__signature__), "(a, b=2)") diff --git a/src/webdav/Collection.py b/src/webdav/Collection.py index 429f8e6179..c54dfbf444 100644 --- a/src/webdav/Collection.py +++ b/src/webdav/Collection.py @@ -31,6 +31,7 @@ from zExceptions import NotFound from zope.datetime import rfc1123_date from zope.interface import implementer +from ZPublisher import zpublish @implementer(IDAVCollection) @@ -59,6 +60,7 @@ def dav__init(self, request, response): # Initialize ETag header self.http__etag() + @zpublish @security.protected(view) def HEAD(self, REQUEST, RESPONSE): """Retrieve resource information without a response body.""" @@ -73,6 +75,7 @@ def HEAD(self, REQUEST, RESPONSE): 'Method not supported for this resource.') raise NotFound('The requested resource does not exist.') + @zpublish def PUT(self, REQUEST, RESPONSE): """The PUT method has no inherent meaning for collection resources, though collections are not specifically forbidden @@ -81,6 +84,7 @@ def PUT(self, REQUEST, RESPONSE): self.dav__init(REQUEST, RESPONSE) raise MethodNotAllowed('Method not supported for collections.') + @zpublish @security.protected(delete_objects) def DELETE(self, REQUEST, RESPONSE): """Delete a collection resource. For collection resources, DELETE diff --git a/src/webdav/NullResource.py b/src/webdav/NullResource.py index fc0444cd82..02deb8ba5f 100644 --- a/src/webdav/NullResource.py +++ b/src/webdav/NullResource.py @@ -52,6 +52,7 @@ from zExceptions import NotFound from zExceptions import Unauthorized from zope.contenttype import guess_content_type +from ZPublisher import zpublish # XXX Originall in ZServer.Zope2.Startup.config @@ -122,6 +123,7 @@ def _default_PUT_factory(self, name, typ, body): return ob + @zpublish @security.public def PUT(self, REQUEST, RESPONSE): """Create a new non-collection resource. @@ -203,6 +205,7 @@ def PUT(self, REQUEST, RESPONSE): RESPONSE.setBody('') return RESPONSE + @zpublish @security.protected(add_folders) def MKCOL(self, REQUEST, RESPONSE): """Create a new collection resource.""" @@ -237,6 +240,7 @@ def MKCOL(self, REQUEST, RESPONSE): RESPONSE.setBody('') return RESPONSE + @zpublish @security.protected(webdav_lock_items) def LOCK(self, REQUEST, RESPONSE): """ LOCK on a Null Resource makes a LockNullResource instance """ @@ -338,11 +342,13 @@ def __init__(self, name): def title_or_id(self): return 'Foo' + @zpublish @security.protected(webdav_access) def PROPFIND(self, REQUEST, RESPONSE): """Retrieve properties defined on the resource.""" return Resource.PROPFIND(self, REQUEST, RESPONSE) + @zpublish @security.protected(webdav_lock_items) def LOCK(self, REQUEST, RESPONSE): """ A Lock command on a LockNull resource should only be a @@ -381,6 +387,7 @@ def LOCK(self, REQUEST, RESPONSE): return RESPONSE + @zpublish @security.protected(webdav_unlock_items) def UNLOCK(self, REQUEST, RESPONSE): """ Unlocking a Null Resource removes it from its parent """ @@ -406,6 +413,7 @@ def UNLOCK(self, REQUEST, RESPONSE): RESPONSE.setStatus(204) return RESPONSE + @zpublish @security.public def PUT(self, REQUEST, RESPONSE): """ Create a new non-collection resource, deleting the LockNull @@ -478,6 +486,7 @@ def PUT(self, REQUEST, RESPONSE): RESPONSE.setBody('') return RESPONSE + @zpublish @security.protected(add_folders) def MKCOL(self, REQUEST, RESPONSE): """ Create a new Collection (folder) resource. Since this is being diff --git a/src/webdav/Resource.py b/src/webdav/Resource.py index d0438e7bb8..94e4c53cae 100644 --- a/src/webdav/Resource.py +++ b/src/webdav/Resource.py @@ -60,12 +60,14 @@ from zope.interface import implementer from zope.lifecycleevent import ObjectCopiedEvent from zope.lifecycleevent import ObjectMovedEvent +from ZPublisher import zpublish from ZPublisher.HTTPRangeSupport import HTTPRangeInterface ms_dav_agent = re.compile("Microsoft.*Internet Publishing.*") +@zpublish @implementer(IDAVResource) class Resource(Base, LockableItem): @@ -199,6 +201,7 @@ def dav__simpleifhandler(self, request, response, method='PUT', return 0 # WebDAV class 1 support + @zpublish @security.protected(view) def HEAD(self, REQUEST, RESPONSE): """Retrieve resource information without a response body.""" @@ -230,6 +233,7 @@ def HEAD(self, REQUEST, RESPONSE): RESPONSE.setStatus(200) return RESPONSE + @zpublish def PUT(self, REQUEST, RESPONSE): """Replace the GET response entity of an existing resource. Because this is often object-dependent, objects which handle @@ -239,6 +243,7 @@ def PUT(self, REQUEST, RESPONSE): self.dav__init(REQUEST, RESPONSE) raise MethodNotAllowed('Method not supported for this resource.') + @zpublish @security.public def OPTIONS(self, REQUEST, RESPONSE): """Retrieve communication options.""" @@ -256,6 +261,7 @@ def OPTIONS(self, REQUEST, RESPONSE): RESPONSE.setStatus(200) return RESPONSE + @zpublish @security.public def TRACE(self, REQUEST, RESPONSE): """Return the HTTP message received back to the client as the @@ -267,6 +273,7 @@ def TRACE(self, REQUEST, RESPONSE): self.dav__init(REQUEST, RESPONSE) raise MethodNotAllowed('Method not supported for this resource.') + @zpublish @security.protected(delete_objects) def DELETE(self, REQUEST, RESPONSE): """Delete a resource. For non-collection resources, DELETE may @@ -303,6 +310,7 @@ def DELETE(self, REQUEST, RESPONSE): return RESPONSE + @zpublish @security.protected(webdav_access) def PROPFIND(self, REQUEST, RESPONSE): """Retrieve properties defined on the resource.""" @@ -322,6 +330,7 @@ def PROPFIND(self, REQUEST, RESPONSE): RESPONSE.setBody(result) return RESPONSE + @zpublish @security.protected(manage_properties) def PROPPATCH(self, REQUEST, RESPONSE): """Set and/or remove properties defined on the resource.""" @@ -345,12 +354,14 @@ def PROPPATCH(self, REQUEST, RESPONSE): RESPONSE.setBody(result) return RESPONSE + @zpublish def MKCOL(self, REQUEST, RESPONSE): """Create a new collection resource. If called on an existing resource, MKCOL must fail with 405 (Method Not Allowed).""" self.dav__init(REQUEST, RESPONSE) raise MethodNotAllowed('The resource already exists.') + @zpublish @security.public def COPY(self, REQUEST, RESPONSE): """Create a duplicate of the source resource whose state @@ -463,6 +474,7 @@ def COPY(self, REQUEST, RESPONSE): RESPONSE.setBody('') return RESPONSE + @zpublish @security.public def MOVE(self, REQUEST, RESPONSE): """Move a resource to a new location. Though we may later try to @@ -591,6 +603,7 @@ def MOVE(self, REQUEST, RESPONSE): # WebDAV Class 2, Lock and Unlock + @zpublish @security.protected(webdav_lock_items) def LOCK(self, REQUEST, RESPONSE): """Lock a resource""" @@ -653,6 +666,7 @@ def LOCK(self, REQUEST, RESPONSE): return RESPONSE + @zpublish @security.protected(webdav_unlock_items) def UNLOCK(self, REQUEST, RESPONSE): """Remove an existing lock on a resource.""" @@ -673,6 +687,7 @@ def UNLOCK(self, REQUEST, RESPONSE): RESPONSE.setStatus(204) # No Content response code return RESPONSE + @zpublish @security.protected(webdav_access) def manage_DAVget(self): """Gets the document source or file data.