# Copyright (c) 2003-2006 gocept gmbh & co. kg # See also LICENSE.txt # """CMF link checker tool - link database""" from AccessControl import getSecurityManager, ClassSecurityInfo from Globals import InitializeClass from Products.BTreeFolder2.BTreeFolder2 import BTreeFolder2 from Products.CMFCore import permissions from gocept.linkchecker import permissions from gocept.linkchecker.interfaces import ILinkDatabase from xmlrpclib import ServerProxy, Fault from zExceptions import Unauthorized import Products.Archetypes.interfaces import Products.CMFCore.utils import gocept.linkchecker.link import gocept.linkchecker.log as log import gocept.linkchecker.url import gocept.linkchecker.utils import socket import zope.interface PROTOCOL_VERSION = 2 WEBSERVICE = "http://lms.gocept.com/v2" WEBSERVICE_STATEMAP = { "unknown" : "grey", "unsupported protocol": "blue", "ok" : "green", "temporary unavailable": "orange", "unavailable": "red"} class OfflineWebserviceDummy(object): """An offline dummy for the web service.""" def __init__(self): self.set_notifications = [] self.register_many = [] self.unregister_many = [] def setClientNotifications(self, option): self.set_notifications.append(option) def registerManyLinks(self, links): self.register_many.append(links) return [(link, 'unknown', 'somereason') for link in links] def unregisterManyLinks(self, links): self.unregister_many.append(links) offline_webservice = OfflineWebserviceDummy() def manage_addLinkDatabase(container, id): """Add a new link database to a link manager.""" container._setObject(id, LinkDatabase(id)) class LinkDatabase(BTreeFolder2): """A database of links. Manages the physical storage of link and url information. """ zope.interface.implements(ILinkDatabase) defaultURLPrefix = "" webservice = WEBSERVICE clientid = "change_me" password = "" offline = False security = ClassSecurityInfo() def getLinkCheckerDatabase(self): """Retrieve this object with a meaningful name via acquisition.""" return self.aq_inner def manage_afterAdd(self, item, container): LinkDatabase.inheritedAttribute('manage_afterAdd')(self, item, container) def insertIndexes(catalog, indexes): existing = catalog.Indexes.objectIds() for index_name, index_type in indexes: if index_name not in existing: catalog.addIndex(index_name, index_type) def insertCacheColumns(catalog, columns): existing_columns = catalog.schema() for c, value in columns: if c not in existing_columns: catalog.addColumn(c, value) # Setup the link catalog if 'link_catalog' not in self.objectIds(): self.manage_addProduct['ZCatalog'].\ manage_addZCatalog('link_catalog', 'Link catalog') indexes = [('state', 'KeywordIndex'), ('url', 'FieldIndex'), ('object', 'KeywordIndex')] insertIndexes(self.link_catalog, indexes) columns = [("object", None), ("url", ""), ("reason", ""), ("lastcheck", ""), ("state", ""), ("link", ""), ("getId", "")] insertCacheColumns(self.link_catalog, columns) # Setup the url catalog if 'url_catalog' not in self.objectIds(): self.manage_addProduct['ZCatalog'].\ manage_addZCatalog('url_catalog', 'URL catalog') indexes = [('url', 'FieldIndex'), ('registered', 'FieldIndex')] insertIndexes(self.url_catalog, indexes) columns = [("url", ""), ("id", ""), ("reason", ""), ("lastcheck", ""), ("state", "grey")] insertCacheColumns(self.url_catalog, columns) ############### # ILinkDatabase security.declareProtected(permissions.ManagePortal, 'configure') def configure(self, defaultURLPrefix=None, clientid=None, password=None, webservice=None, client_notifications=None): """Set configuration values.""" if clientid is not None: self.clientid = clientid if password: self.password = password if defaultURLPrefix is not None: self.defaultURLPrefix = defaultURLPrefix if webservice is not None: self.webservice = webservice # need to reconfigure connection try: delattr(self, '_v_lms_client') except AttributeError: pass if client_notifications is not None: server = self._getWebServiceConnection() if server is not None: server.setClientNotifications(client_notifications) security.declareProtected( permissions.USE_LINK_MANAGEMENT, 'registerLinks') def registerLinks(self, links, object, online=True): """Registers links for an object at the database.""" sm = getSecurityManager() if not sm.checkPermission( permissions.ModifyPortalContent, object): raise Unauthorized, \ "Can't modify link registrations for this object." link_objects = [self._register_link(link, object) for link in links] if online: urls = [link.getURL() for link in link_objects if link is not None] self._register_urls_at_lms(urls) def _register_link(self, link, object): link_id = gocept.linkchecker.utils.hash_link(link, object) if link_id in self.objectIds(): return link = gocept.linkchecker.link.Link(link, link_id, object.UID()) # Now we can add the link to the database self._setObject(link_id, link) return self[link_id] security.declarePrivate("_updateWSRegistrations") def _updateWSRegistrations(self): """Registers a link with the webservice. May also register yet non-registered other links. """ unregistered = self.queryURLs(registered=False) unregistered = [x.getObject() for x in unregistered] # Nothing to do? if not unregistered: return self._register_urls_at_lms(unregistered) def _register_urls_at_lms(self, url_objects): """Register the given URL objects at the LMS web service. """ # Do we have an LMS connection? lms = self._getWebServiceConnection() if lms is None: return urls = [url.url for url in url_objects] states = lms.registerManyLinks(urls) # The server *may* report the status of URls it already knows. for url, state, reason in states: state = WEBSERVICE_STATEMAP[state] url = self[gocept.linkchecker.utils.hash_url(url)] url.updateStatus(state, reason) # Mark all of the URLs as registered for url in url_objects: url.registered = True security.declarePrivate('unregisterLink') def unregisterLink(self, link, object): """Unregisters a given link/object pair at the database. If the given link isn't referenced by any object anymore, it will be removed from the database completely. Returns None. """ id = gocept.linkchecker.utils.hash_link(link) self.manage_delObject(id) security.declarePrivate('unregisterObject') def unregisterObject(self, object): """Unregisters all links for this object. Returns None. """ if isinstance(object, (gocept.linkchecker.url.URL, gocept.linkchecker.link.Link)): return links = self.getLinksForObject(object) link_ids = [x.getId() for x in links] self.manage_delObjects(link_ids) def _getWebServiceConnection(self): """return connection to lms returns LMSClient instance if connection is ok returns None if connection is not Ok """ conn = self.checkConnection() if conn.ok: client = self._get_connection_object() else: log.logger.error('Connection to LMS not possible: %s' % (conn.error, )) if self.offline: client = offline_webservice else: client = None return client def _get_connection_object(self): client = getattr(self, '_v_lms_client', None) if client is None: client = LMSClient(self.webservice, self.clientid, self.password) self._v_lms_client = client return client def _deleteURLs(self, urls): # Unregister with LMS lms = self._getWebServiceConnection() if lms is None: return self.manage_delObjects([url.id for url in urls]) lms.unregisterManyLinks([url.url for url in urls]) security.declareProtected(permissions.USE_LINK_MANAGEMENT, 'getLinksForURL') def getLinksForURL(self, url): """Retrieve links for an url.""" links = [x.getObject() for x in self.queryLinks(url=url)] links = filter(None, links) return links security.declareProtected(permissions.USE_LINK_MANAGEMENT, 'getLinksForObject') def getLinksForObject(self, object, state=None): """Retrieve information about an object. Returns a list of ILink objects. Returns an empty list if the object hasn't been registered yet. """ sm = getSecurityManager() if not sm.checkPermission( permissions.AccessContentsInformation, object): raise Unauthorized, "Can't view links on this object." if not Products.Archetypes.interfaces.IReferenceable.providedBy(object): return [] uid = object.UID() if uid is None: links = [] else: query_args = {} if state is not None: query_args['state' ] = state links = self.queryLinks(object=[uid], **query_args) links = [x.getObject() for x in links] links = filter(None, links) return links security.declareProtected(permissions.USE_LINK_MANAGEMENT, 'getAllLinks') def getAllLinks(self): """Returns a list of all ILink objects. Warning: This is likely to be a slow method in large data sets. """ return self.objectValues(['Link']) security.declareProtected(permissions.USE_LINK_MANAGEMENT, 'getAllLinkIds') def getAllLinkIds(self): """Returns a list of the ids of all ILink objects. Warning: This is likely to be a slow method in large data sets. """ return self.objectIds(['Link']) security.declareProtected( permissions.USE_LINK_MANAGEMENT, 'getLinkIterator') def getLinkIterator(self): """returns generator for all link objects""" for id in self.getAllLinkIds(): yield self.get(id) security.declareProtected(permissions.USE_LINK_MANAGEMENT, 'getLinkCount') def getLinkCount(self): """Returns the amount of links currently in the database.""" return len(self.queryLinks()) security.declareProtected(permissions.USE_LINK_MANAGEMENT, 'queryLinks') def queryLinks(self, **args): """Returns the result of querying the ZCatalog that indexes links.""" return self.link_catalog(**args) security.declareProtected(permissions.USE_LINK_MANAGEMENT, 'queryURLs') def queryURLs(self, **args): """Returns the result of querying the ZCatalog that indexes urls.""" return self.url_catalog(**args) security.declareProtected( permissions.ManagePortal, 'sync') def sync(self): lms = self._getWebServiceConnection() lms.forceSynchronization() security.declareProtected( permissions.ManagePortal, 'updateLinkStatus') def updateLinkStatus(self, url, state, reason): """XML-RPC connector for LMS""" state = WEBSERVICE_STATEMAP[state] for url in self.queryURLs(url=url): url = url.getObject() url.updateStatus(state, reason) security.declarePublic('updateManyStates') def updateManyStates(self, client_id, password, update_list): """XML-RPC connector for LMS""" self.authenticateXMLRPC(client_id, password) for url, state, reason in update_list: self.updateLinkStatus(url, state, reason) security.declarePublic('xmlrpc_getAllLinks') def xmlrpc_getAllLinks(self, client_id, password): """XML-RPC connector for LMS returns all links for LMS """ self.authenticateXMLRPC(client_id, password) # XXX optimiziation: make .url unique return [x.url for x in self.queryURLs()] security.declarePrivate('authenticateXMLRPC') def authenticateXMLRPC(self, client_id, password): """authenticates XML remote call""" if not (client_id == self.clientid and\ password == self.password): raise Unauthorized,\ "Client-Id or password do not match." security.declareProtected( permissions.ManagePortal, 'updateAllStatus') def updateAllStatus(self): """Manually update all link status' """ server = self._getWebServiceConnection() if server is None: return for url in self.queryURLs(): try: status, reason = server.getStatus(url.url) except KeyError: # not registered with server server.registerLink(url.url) status = 'temporary unavailable' reason = 'No status response from LMS yet.' status = WEBSERVICE_STATEMAP[status] if url.state != status: url = url.getObject() url.updateStatus(status, reason) security.declareProtected( permissions.USE_LINK_MANAGEMENT, 'checkConnection') def checkConnection(self): """return if connection is up and running returns r.ok == True if connection is ok returns r.ok == False if connection is *not* ok sets also r.error to some error information """ class R: __allow_access_to_unprotected_subobjects__ = 1 def __init__(self, **kwargs): self.__dict__.update(kwargs) try: server = self._get_connection_object() except Exception, e: ok = False error = '%s: %s' % (str(e.__class__), str(e)) protocol_version='unknown', else: ok, protocol_version, error = server.checkConnection() r = R(ok=ok, error=error, client_protocol=PROTOCOL_VERSION, server_protocol=protocol_version) return r security.declareProtected( permissions.ManagePortal, 'getClientNotifications') def getClientNotifications(self): server = self._getWebServiceConnection() if server is None: return False stat = server.getClientNotifications() return stat security.declareProtected(permissions.ManagePortal, 'getInfoFrameURL') def getInfoFrameURL(self): """Returns the URL received from the LMS or an empty string.""" server = self._getWebServiceConnection() if server is None: return "" return server.getInfoFrameURL() class LMSClient: """client to lms""" _exception_mapping = { 'KeyError': KeyError, } def __init__(self, url, client_id, password): self.url = url self.client_id = client_id self.password = password self._server = ServerProxy(url) def forceSynchronization(self): self._server.forceSynchronization(self.client_id, self.password) def registerManyLinks(self, urls): result = [] if urls: try: result = self._server.registerURLs(self.client_id, self.password, urls) except Fault, f: self._translate_fault(f) return result def unregisterManyLinks(self, urls): if not urls: return [] try: result = self._server.unregisterURLs(self.client_id, self.password, urls) except Fault, f: self._translate_fault(f) return result def getStatus(self, url): try: status = self._server.getStatus(url) except Fault, f: self._translate_fault(f) return status def getClientNotifications(self): try: return self._server.getClientNotifications(self.client_id, self.password) except Fault, f: self._translate_fault(f) def getInfoFrameURL(self): try: return self._server.getInfoFrameURL(self.client_id, self.password) except Fault, f: self._translate_fault(f) def setClientNotifications(self, status): if isinstance(status, (str, unicode)): status = int(status) if isinstance(status, int): status = bool(status) self._server.setClientNotifications(self.client_id, self.password, status) def checkConnection(self): is_ok = False error = 'unknown' protocol_version = None try: protocol_version = self._server.checkConnection(self.client_id, self.password) except Fault, f: error = '%s (%s)' % (f.faultString, f.faultCode) except socket.error, e: e_code, e_string = e.args error = 'socket.error: %s (%s)' % (e_string, e_code) except Exception, e: error = str(e) else: if protocol_version == PROTOCOL_VERSION: is_ok = True else: error = ("Incompatible protocol version. " "Got: %s, expected: %s" % (protocol_version, PROTOCOL_VERSION)) return is_ok, protocol_version, error ######### # private def _translate_fault(self, f): name, value = f.faultCode, f.faultString exception_class = self._exception_mapping.get(name) if exception_class is None: sm = zope.component.getSiteManager() Products.CMFCore.utils.getToolByName( sm, "plone_utils").addPortalMessage( "LMS Error: %s/%s" % (name, value), type='error') else: e = exception_class(value) raise e InitializeClass(LinkDatabase)