from AccessControl.SecurityInfo import ClassSecurityInformation from Acquisition import aq_inner from BTrees.IOBTree import IOBTree from Products.CMFCore.utils import getToolByName from Products.CMFPlone.interfaces import IPloneSiteRoot from Products.Transience.Transience import Increaser from ftw.publisher.core import states from ftw.publisher.sender import extractor from ftw.publisher.sender.interfaces import IConfig from ftw.publisher.sender.interfaces import IOverriddenRealmRegistry from ftw.publisher.sender.interfaces import IQueue from ftw.publisher.sender.interfaces import IRealm from persistent import Persistent from persistent.dict import PersistentDict from persistent.list import PersistentList from plone.memoize import instance from zope import interface, component from zope.annotation.interfaces import IAnnotations from zope.annotation.interfaces import IAttributeAnnotatable import os import time _marker = object() ANNOTATIONS_PATH_BLACKLIST_KEY = 'publisher-path-blacklist' class Config(object): """ The Config object is registered via zcml as adapter. It stores the configured realms """ interface.implements(IConfig) component.adapts(IPloneSiteRoot) security = ClassSecurityInformation() def __init__(self, context): """ Constructor: load the annotations, which are stored on the plone site. @param context: any context @type context: Plone object """ # get plone site self.context = aq_inner(context.portal_url.getPortalObject()) # get annotations for plone site self.annotations = IAnnotations(self.context) security.declarePublic('is_update_realms_possible') def is_update_realms_possible(self): return self._get_realm_registry() is None security.declarePrivate('getRealms') def getRealms(self): """ Returns a PersistentList of Realm objects @return: Realm objects @rtype: PersistentList """ registry = self._get_realm_registry() if registry: return tuple(registry.realms) else: return self.annotations.get('publisher-realms', PersistentList()) security.declarePrivate('_setRealms') def _setRealms(self, list): """ Stores a PersistentList of Realm objects @param list: Realm objects @type list: PersistentList @return: None """ if not self.is_update_realms_possible(): raise AttributeError( 'The realm registry is overridden and not mutatable.') if not isinstance(list, PersistentList): raise TypeError('Excpected PersistentList') self.annotations['publisher-realms'] = list security.declarePrivate('appendRealm') def appendRealm(self, realm): """ Appends a Realm to the realm list @param realm: Realm object @type realm: Realm @return: None """ if not IRealm.providedBy(realm): raise TypeError('Excpected Realm object') list = self.getRealms() list.append(realm) self._setRealms(list) security.declarePrivate('removeRealm') def removeRealm(self, realm): """ Removes a Realm from the realm list @param realm: Realm object @type realm: Realm @return: None """ if not isinstance(realm, Realm): raise TypeError('Excpected Realm object') list = self.getRealms() list.remove(realm) self._setRealms(list) security.declarePrivate('_get_realm_registry') def _get_realm_registry(self): return component.queryUtility(IOverriddenRealmRegistry) security.declarePrivate('getDataFolder') @instance.memoize def getDataFolder(self): """ Returns the path to the data folder. If it does not exist, it will be created. @return: absolute file system path @rtype: string """ path = os.path.join('/'.join( os.environ['CLIENT_HOME'].split('/')[:-2] + ['var', 'publisher'])) # create if not existing if not os.path.exists(path): os.makedirs(path) return path security.declarePrivate('get_executed_folder') def get_executed_folder(self): executed_folder = os.path.join(self.getDataFolder(), 'executed') if not os.path.exists(executed_folder): os.makedirs(executed_folder) return executed_folder security.declarePrivate('getPathBlacklist') def getPathBlacklist(self): """ Returns a list of paths which are blacklistet and are not touched by the publisher. """ blacklist = self.annotations.get(ANNOTATIONS_PATH_BLACKLIST_KEY, _marker) if blacklist is _marker: blacklist = PersistentList() self.setPathBlacklist(blacklist) return blacklist security.declarePrivate('setPathBlacklist') def setPathBlacklist(self, blacklist): """ Sets the path blacklist """ if not isinstance(blacklist, PersistentList): raise ValueError('Expected PersistentList, got %s' % repr(blacklist)) self.annotations[ANNOTATIONS_PATH_BLACKLIST_KEY] = blacklist security.declarePrivate('appendPathToBlacklist') def appendPathToBlacklist(self, path): """ Appends a path to the blacklist, if it isnt already blacklisted... """ if type(path) not in (str, unicode): raise ValueError('Expected string, got %s' % repr(path)) path = path.strip() blacklist = self.getPathBlacklist() if path not in blacklist: blacklist.append(path) self.setPathBlacklist(blacklist) security.declarePrivate('removePathFromBlacklist') def removePathFromBlacklist(self, path): """ Removes a path from the path blacklist """ if type(path) not in (str, unicode): raise ValueError('Expected string, got %s' % repr(path)) path = path.strip() blacklist = self.getPathBlacklist() if path in blacklist: blacklist.remove(path) return True else: return False security.declarePrivate('publishing_enabled') def publishing_enabled(self): """ Returns True if the publishing is enable at the moment """ return self.annotations.get('publisher-publishing-enabled', True) security.declarePrivate('set_publishing_enabled') def set_publishing_enabled(self, enabled): self.annotations['publisher-publishing-enabled'] = bool(enabled) security.declarePrivate('locking_enabled') def locking_enabled(self): """ Returns True if locking is enabled, default is True """ return self.annotations.get('publisher-locking-enabled', True) security.declarePrivate('set_locking_enabled') def set_locking_enabled(self, enabled): if not isinstance(enabled, bool): enabled = bool(enabled == '1') self.annotations['publisher-locking-enabled'] = bool(enabled) security.declarePrivate('get_ignored_fields') def get_ignored_fields(self): """ Returns a PersistentDict if there are no ignored-fields yet. """ return self.annotations.get('publisher-ignored-fields', PersistentDict()) security.declarePrivate('set_ignored_fields') def set_ignored_fields(self, dictionary): """ Sets the publisher-ignored-fields. Example: {'FormSaveDataAdapter': ['SavedFormInput',]} """ self.annotations['publisher-ignored-fields'] = dictionary class Queue(object): """ The Queue adapter stores a list of Jobs to process. """ interface.implements(IQueue) component.adapter(IPloneSiteRoot) security = ClassSecurityInformation() def __init__(self, context): """ Constructor: load the annotations, which are stored on the plone site. @param context: any context @type context: Plone object """ self.context = aq_inner(context.portal_url.getPortalObject()) self.annotations = IAnnotations(self.context) security.declarePrivate('getJobs') def getJobs(self): """ Returns a PersistentList of Job objects @return: job-objects @rtype: PersistentList """ return self.annotations.get('publisher-queue', PersistentList()) security.declarePrivate('_setJobs') def _setJobs(self, list): """ Stores a PersistentList of Job objects @param list: list of jobs @type list: PersistentList @return: None """ if not isinstance(list, PersistentList): raise TypeError('Excpected PersistentList') self.annotations['publisher-queue'] = list security.declarePrivate('appendJob') def appendJob(self, job): """ Appends a Job to the queue @param job: Job object @type: Job @return: None """ if not isinstance(job, Job): raise TypeError('Excpected Job object') list = self.getJobs() list.append(job) self._setJobs(list) security.declarePrivate('createJob') def createJob(self, *args, **kwargs): """ Creates a new Job object, adds it to the queue and returns it. Arguments are redirected to the Job-Constructor. @return: Job object @rtype: Job """ job = Job(*args, **kwargs) self.appendJob(job) return job security.declarePrivate('removeJob') def removeJob(self, job): """ Removes a Job from the queue @param job: Job object @type job: Job @return: None """ if not isinstance(job, Job): raise TypeError('Excpected Job object') list = self.getJobs() list.remove(job) self._setJobs(list) security.declarePrivate('countJobs') def countJobs(self): """ Returns the amount of jobs in the queue. Used in combination with popJob() @return: Amount of jobs in the queue @rtype: int """ return len(self.getJobs()) security.declarePrivate('popJob') def popJob(self): """ Returns the oldest Job from the queue. The Job will be removed from the queue immediately! @return: Oldest Job object @rtype: Job """ return self.getJobs().pop(0) security.declarePrivate('_get_executed_jobs_storage') def _get_executed_jobs_storage(self): """Returns the IOBTree storage object for executed jobs. """ if self.annotations.get('publisher-executed', _marker) == _marker: self.annotations['publisher-executed'] = IOBTree() return self.annotations['publisher-executed'] security.declarePrivate('_generate_next_executed_jobs_storage_key') def _generate_next_executed_jobs_storage_key(self): """Returns a transaction-safe auto-increment value http://pyyou.wordpress.com/2009/12/09/how-to-add-a-counter-without-conflict-error-in-zope """ ann_key = 'publisher-executed-key-auto-increment' # get the increaser stored in the annotations if ann_key not in self.annotations.keys(): self.annotations[ann_key] = Increaser(0) inc = self.annotations[ann_key] # increase by one inc.set(inc() + 1) # check the current max key try: current_max_key = self._get_executed_jobs_storage().maxKey() except ValueError: # the storage is empty, start with 0 current_max_key = 0 while current_max_key >= inc(): inc.set(inc() + 1) # set and return the new value self.annotations[ann_key] = inc return inc() security.declarePrivate('get_executed_jobs') def get_executed_jobs(self, start=0, end=None): """Returns a iterator of executed jobs. You can make a batch by providing a range with `start` and `end` parameters. The start and end parameters represent the index number in the storage, so we can expect that the length of the iterate is `end - start`, if there are enough objects in the storage. A key / value pair is returned. """ data = self._get_executed_jobs_storage() if start == 0 and end == None: # return all return data.iteritems() elif start > end or start == end: return () else: # make a batch without touching unused values # the iteritems() method wants the min-key and the # max-key which is used as filter then. So we need to map # our `start` and `end` (which is the index) to the max- # and min-*keys* keys = data.keys()[start:end] return data.iteritems(min(keys), max(keys)) security.declarePrivate('get_executed_jobs_length') def get_executed_jobs_length(self): """Returns the amount of currently stored executed jobs. """ return len(self._get_executed_jobs_storage().keys()) security.declarePrivate('append_executed_job') def append_executed_job(self, job): """Add another """ data = self._get_executed_jobs_storage() key = self._generate_next_executed_jobs_storage_key() data.insert(key, job) return key security.declarePrivate('remove_executed_job') def remove_executed_job(self, key, default=None): """Removes the job with the `key` from the executed jobs storage. """ return self._get_executed_jobs_storage().pop(key, default) security.declarePrivate('get_executed_job_by_key') def get_executed_job_by_key(self, key): """Returns a executed job according to its internal storage key """ return self._get_executed_jobs_storage()[key] security.declarePrivate('clear_executed_jobs') def clear_executed_jobs(self): """Removes all jobs from the executed jobs storage. """ for key, job in self.get_executed_jobs(): job.removeJob() self._get_executed_jobs_storage().clear() security.declarePrivate('remove_executed_jobs_older_than') def remove_executed_jobs_older_than(self, time): """Removes all executed jobs which are older than `time` (datetime instance). """ def _get_date_of_job(job): """Returns the date of the newest execution of this job or None. """ if getattr(job, 'executed_list', None) == None: return None elif len(job.executed_list) == 0: return None else: return job.executed_list[-1]['date'] # start end data = self._get_executed_jobs_storage() for key in tuple(data.keys()): job = data.get(key) date = _get_date_of_job(job) if not date: continue elif date < time: self.remove_executed_job(key) job.removeJob() else: break security.declarePrivate('remove_jobs_by_filter') def remove_jobs_by_filter(self, filter_method): """Removs jobs by a filter method. The `filter_method` gets the `key` and the `job` as parameters. If it returns `True` the job os deleted. """ for key, job in tuple(self.get_executed_jobs()): if filter_method(key, job): self.remove_executed_job(key) job.removeJob() class Job(Persistent): """ A Job object contains action, object and the user who triggered the job. It is stored in the Queue and is executed asynchronous. """ security = ClassSecurityInformation() def __init__(self, action, object, username): """ Constructor: sets the given arguments. @param action: action type [push|delete] @type action: string @param object: plone object to run job on @type object: Plone object @param username: Name of the user which performed the action @type username: string """ super(Persistent, self).__init__() self.is_root = IPloneSiteRoot.providedBy(object) self.action = action self.username = username self.objectPath = '/'.join(object.getPhysicalPath()) # store the path as uid if we are on a plone root self.objectUID = self.is_root and self.objectPath or object.UID() self.objectTitle = object.pretty_title_or_id() self._extractData(object) security.declarePrivate('_extractData') def _extractData(self, object): """ Extracts the data from the object and stores the JSON string in a cache-file. @param object: plone object to run job on @type object: Plone object """ # create new data file dir = Config(object).getDataFolder() i = 1 file = None while not file: filename = '%s.%s.%s.json' % ( # on plone root we use the path as uid, now we have to replace # the '/' by '_' to provide a good name self.objectUID.replace('/', '_'), time.strftime('%Y%m%d-%H%M%S'), str(i).rjust(3, '0'), ) file = os.path.join(dir, filename) if os.path.exists(file): file = None f = open(file, 'w') # extract data data = extractor.Extractor()(object, self.action) # write data f.write(data) f.close() self.dataFile = file security.declarePrivate('getSize') def getSize(self): try: return len(self.getData()) except IOError: return -1 security.declarePrivate('getData') def getData(self): """ Loads the JSON-data from the cache file and returns the JSON-string @return: JSON data @rtype: string """ f = open(self.dataFile) data = f.read() f.close() return data security.declarePrivate('removeJob') def removeJob(self): """ Removes the cache file for this job from the file system """ if os.path.exists(self.dataFile): os.remove(self.dataFile) security.declarePrivate('json_file_exists') def json_file_exists(self): return os.path.exists(self.dataFile) security.declarePrivate('move_jsonfile_to') def move_jsonfile_to(self, dir): oridir, oname = os.path.split(self.dataFile) obase = '.'.join(oname.split('.')[:-1]) oext = oname.split('.')[-1] i = 1 path = os.path.join(dir, oname) # find new unused name while os.path.exists(path): i += 1 path = os.path.join(dir, '%s.%i.%s' % ( obase, i, oext)) # move it os.rename(self.dataFile, path) self.dataFile = path security.declarePrivate('getObject') def getObject(self, context): """ Returns the object with UID stored in this Job. This method requires any context for getting the reference_catalog. @param context: Any Plone object @type context: Plone Object @return: Context object of this Job @rtype: Plone object """ reference_tool = getToolByName(context, 'reference_catalog') return self.is_root and context.portal_url.getPortalObject() or \ reference_tool.lookupObject(self.objectUID) security.declarePrivate('executed_with_states') def executed_with_states(self, entries): """ entries: {'date': <datetime ...>, <Realm1 ..>: <State1 ..>, <Realm2 ..>: <State2 ..>} """ if not getattr(self, 'executed_list', None): self.executed_list = PersistentList() self.executed_list.append(entries) security.declarePrivate('get_latest_executed_entry') def get_latest_executed_entry(self): entries_list = getattr(self, 'executed_list', ()) # get the last entries if len(entries_list) == 0: return None last_entries = entries_list[-1] # then return the first "bad" entry or the last for state in last_entries.values(): if isinstance(state, states.ErrorState): return state return state security.declarePrivate('get_filename') def get_filename(self): return os.path.split(self.dataFile)[1] class Realm(Persistent): """ A Realm object provides information about a target plone instance (receiver) which should have installed ftw.publisher.receiver. It stores and provides information such as URL or credentials. URL+username should be unique! """ interface.implements(IAttributeAnnotatable, IRealm) security = ClassSecurityInformation() active = 0 url = '' username = '' password = '' def __init__(self, active, url, username, password): """ Constructor: stores the given arguments in the object @param active: Is this realm active? @type active: boolean or int @param url: URL to the plone site of the target realm @type url: string @param username: Username to login on target realm @type username: string @param password: Password of the User with **username** @type password: string """ self.active = active and 1 or 0 self.url = url self.username = username self.password = password