Doc + facebook object instanciation

<?php

/**
 *
 * @package    sfFacebookConnectPlugin
 * @author     Fabrice Bernhard
 *
 */
class sfFacebook
{
  protected static
    $client          = null; // Facebook instance
  protected static
    $guard_adapter   = null;
  protected static
    $is_js_loaded       = false;

  /**
   * gets the facebook client instance
   *
   * @return Facebook
   * @author fabriceb
   * @since 2009-05-17
   * @since 2010-05-12 Benjamin Grandfond <benjaming@theodo.fr>: new Facebook php-sdk
   */

  public static function getFacebookCookie()
  {
    $app_id = self::getApiKey();
    $application_secret = self::getApiSecret();
    $args = array();
    if (!isset($_COOKIE['fbs_' . $app_id]))
    {
      return null;
    }
    parse_str(trim($_COOKIE['fbs_' . $app_id], '\\"'), $args);
    ksort($args);
    $payload = '';
    foreach ($args as $key => $value)
    {
      if ($key != 'sig')
      {
        $payload .= $key . '=' . $value;
      }
    }
    if (md5($payload . $application_secret) != $args['sig'])
    {
      return null;
    }
    return $args;
  }

  public static function getFacebookClient()
  {
    if (self::$client === null)
    {
      $params = array(
        'appId'  => self::getApiKey(),
        'secret' => self::getApiSecret(),
        'cookie' => self::getApiCookie(),
        'domain' => self::getApiDomain()
      );

      self::$client = new Facebook($params);
    }

    if (!self::$client)
    {
      error_log('Could not create facebook client.');
    }

    return self::$client;
  }

  /**
   * get the facebook session
   *
   * @return Array
   * @author Benjamin Grandfond <benjaming@theodo.fr>
   * @since 2010-05-13
   */
  public static function getSession()
  {

    return self::getFacebookClient()->getSession();
  }

  /**
   * get the facebook user
   *
   * @return Array
   * @author Benjamin Grandfond <benjaming@theodo.fr>
   * @since 2010-05-13
   */
  public static function getUser()
  {

    return self::getFacebookClient()->getUser();
  }

  /**
   * get datas through the faceook graph api
   * @see http://developers.facebook.com/docs/api
   *
   * @return Array
   * @author Benjamin Grandfond <benjaming@theodo.fr>
   * @since 2010-05-13
   */
  public static function getFacebookApi($param)
  {

    return self::getFacebookClient()->api($param);
  }

   /**
   * gets the facebook api key
   *
   * @return Facebook
   * @author fabriceb
   * @since 2009-05-17
   */
  public static function getApiKey()
  {

    return sfConfig::get('app_facebook_api_key');
  }

   /**
   * gets the facebook api secret
   *
   * @return Facebook
   * @author fabriceb
   * @since 2009-05-17
   */
  public static function getApiSecret()
  {

    return sfConfig::get('app_facebook_api_secret');
  }

  /**
   * get the facebook app cookie support
   *
   * @return Boolean
   * @author Benjamin Grandfond <benjaming@theodo.fr>
   * @since 2010-05-12
   */
  public static function getApiCookie()
  {

    return sfConfig::get('app_facebook_api_cookie', true);
  }

  /**
   * get the facebook app domain
   *
   * @return Boolean
   * @author Benjamin Grandfond <benjaming@theodo.fr>
   * @since 2010-05-12
   */
  public static function getApiDomain()
  {

    return sfConfig::get('app_facebook_api_domain', null);
  }

  /**
   * gets or create user with facebook uid inprofile
   *
   * @param Integer $facebook_uid
   * @param boolean $isActive
   * @return sfGuardUser $sfGuardUser
   */
  public static function getOrCreateUserByFacebookUid($facebook_uid, $isActive = true)
  {
    $sfGuardUser = self::getGuardAdapter()->getSfGuardUserByFacebookUid($facebook_uid, $isActive);

    if (!$sfGuardUser instanceof sfGuardUser)
    {
      if (sfConfig::get('sf_logging_enabled'))
      {
        sfContext::getInstance()->getLogger()->info('{sfFacebookConnect} No user exists with current email hash');
      }
      $sfGuardUser = self::getGuardAdapter()->createSfGuardUserWithFacebookUid($facebook_uid);
    }

    return $sfGuardUser;
  }

  /**
   * gets user with facebook uid inprofile
   *
   * @param Integer $facebook_uid
   * @param boolean $isActive
   * @return sfGuardUser $sfGuardUser
   */
  public static function getUserByFacebookUid($facebook_uid, $isActive = true)
  {
    $sfGuardUser = self::getGuardAdapter()->retrieveSfGuardUserByFacebookUid($facebook_uid, $isActive);

    if (!$sfGuardUser instanceof sfGuardUser)
    {
      if (sfConfig::get('sf_logging_enabled'))
      {
        sfContext::getInstance()->getLogger()->info('{sfFacebookConnect} No user exists with current email hash');
      }
    }

    return $sfGuardUser;
  }

  /**
   * Gets the currently logged sfGuardUser using Facebook Session
   *
   * @param boolean $create whether to automatically create a sfGuardUser
   * if none found corresponding to the Facebook session
   * @param boolean $isActive
   * @return sfGuardUser
   * @author fabriceb
   * @since 2009-05-17
   * @since 2009-08-25
   */
  public static function getSfGuardUserByFacebookSession($create = true, $isActive = true)
  {
    // We get the facebook uid from session
    $fb_uid = self::getAnyFacebookUid();
    if ($fb_uid)
    {

      if ($create)
      {

        return self::getOrCreateUserByFacebookUid($fb_uid, $isActive);
      }
      else
      {

        return self::getUserByFacebookUid($fb_uid, $isActive);
      }
    }

    if (sfConfig::get('sf_logging_enabled'))
    {
      sfContext::getInstance()->getLogger()->info('{sfFacebookConnect} No current Facebook session');
    }

    return null;
  }

  /**
   * checks the existence of the HTTP_X_FB_USER_REMOTE_ADDR porperty in the header
   * which is a sign of being included by the fbml interface
   *
   * @return boolean
   * @author fabriceb
   * @since Jun 8, 2009 fabriceb
   */
  public static function isInsideFacebook()
  {

    return isset($_SERVER['HTTP_X_FB_USER_REMOTE_ADDR']);
  }

  /**
   *
   * @return boolean
   * @author fabriceb
   * @since Jun 8, 2009 fabriceb
   */
  public static function inCanvas()
  {

    return self::getFacebookClient()->in_fb_canvas();
  }

  /**
   * redirects to the login page of the Facebook application if not logged yet
   *
   * @author fabriceb
   * @since Jun 8, 2009 fabriceb
   */
  public static function requireLogin()
  {
    self::getFacebookClient()->require_login();
  }

  /**
   * redirects depnding on in canvas or not
   *
   * @param $url
   * @param $statusCode
   * @return mixed sfView::NONE or sfStopException
   * @author fabriceb
   * @since Jun 8, 2009 fabriceb
   */
  public static function redirect($url, $statusCode = 302)
  {

    $context = sfContext::getInstance();
    $response = $context->getResponse();

    if (self::inCanvas())
    {
      $url = sfConfig::get('app_facebook_app_url').$context->getController()->genUrl($url, false);

      $text = '<fb:redirect url="' . $url . '"/>';
      $response->setContent(sfContext::getInstance()->getResponse()->getContent().$text);

      return sfView::NONE;
    }
    else
    {
      $fb_parameters = '?'.sfFacebook::getFacebookSigParameters(sfContext::getInstance()->getRequest());
      $url = $context->getController()->genUrl($url, true).$fb_parameters;

      $response->clearHttpHeaders();
      $response->setStatusCode($statusCode);
      $response->setHttpHeader('Location', $url);
      $response->setContent(sprintf('<meta http-equiv="refresh" content="%d;url=%s"/>', 0, htmlspecialchars($url, ENT_QUOTES, sfConfig::get('sf_charset'))));
      $response->send();

      throw new sfStopException();
    }
  }

  /**
   *
   * @param integer $user_uid
   * @return integer[]
   * @author fabriceb
   * @since Jun 9, 2009 fabriceb
   */
  public static function getFacebookFriendsUids($user_uid = null)
  {

    try
    {
      $friends_uids = self::getFacebookApi()->friends_get(null, $user_uid);
    }
    catch(FacebookRestClientException $e)
    {
      $friends_uids = array();
      if (sfConfig::get('sf_logging_enabled'))
      {
        sfContext::getInstance()->getLogger()->info('{FacebookRestClientException} '.$e->getMessage());
      }
    }

    return $friends_uids;
  }

  /**
  *
  * @return sfFacebookGuardAdapter
  * @author fabriceb
  * @since Aug 10, 2009
  * @since 2009-10-08 Alban Creton : added configurability of the Guard Adapter.
  */
  public static function getGuardAdapter()
  {
    if (self::$guard_adapter === null)
    {
      if(sfConfig::get('app_facebook_guard_adapter') && class_exists(sfConfig::get('app_facebook_guard_adapter'), true))
      {
        $class = sfConfig::get('app_facebook_guard_adapter');
      }
      else if (class_exists('sfGuardUserPeer', true))
      {
        $class = 'sfFacebookPropelGuardAdapter';
      }
      else
      {
        $class = 'sfFacebookDoctrineGuardAdapter';
      }
      self::$guard_adapter = new $class();
    }
    if (!self::$guard_adapter)
    {
      error_log('Could not create guard adapter.');
    }

    return self::$guard_adapter;
  }

  /**
   *
   * @return boolean
   * @author fabriceb
   * @since Aug 27, 2009
   */
  public static function isJsLoaded()
  {

    return self::$is_js_loaded;
  }

  /**
   *
   * @return void
   * @author fabriceb
   * @since Aug 27, 2009
   */
  public static function setJsLoaded()
  {
    self::$is_js_loaded = true;
  }

  /**
   * Dirty way to convert fr into fr_FR
   * @param string $culture
   * @return string
   * @author fabriceb
   * @since Aug 28, 2009
   */
  public static function getLocale($culture = null)
  {
    if (is_null($culture))
    {
      $culture = sfContext::getInstance()->getUser()->getCulture();
    }

    $culture_to_locale = array(
      'fr' => 'fr_FR',
      'en' => 'en_US',
      'de' => 'de_DE',
      'it' => 'it_IT',
    );

    return array_key_exists($culture, $culture_to_locale) ? $culture_to_locale[$culture] : $culture;
  }

  /**
  * @return interger $facebook_uid
  * @author fabriceb
  * @since Oct 6, 2009
  */
  public static function getAnyFacebookUid()
  {

    $cookie = self::getFacebookCookie();
    $fb_uid = $cookie['uid'];
    sfContext::getInstance()->getLogger()->info('{sfFacebookConnect} Fb_uid from cookie : '.$fb_uid);

    return $fb_uid;
  }

  /**
   *
   * @param sfWebRequest $request
   * @return string
   * @author fabriceb
   * @since Oct 12, 2009
   */
  public static function getFacebookSigParameters(sfWebRequest $request)
  {
    $parameters = $request->getParameterHolder()->getAll();

    $parameter_string = '';
    foreach ($parameters as $key => $parameter)
    {
      if (substr($key,0,3)=='fb_')
      {
        $parameter_string .= '&'.$key.'='.$parameter;
      }
    }

    return $parameter_string;
  }

}

<?php

/**
 *
 * @package    sfFacebookConnectPlugin
 * @author     Fabrice Bernhard
 *
 */
class sfFacebook
{
  protected static
    $client          = null; // Facebook instance
  protected static
    $guard_adapter   = null;
  protected static
    $is_js_loaded       = false;

  public static function getFacebookCookie()
  {
    $app_id = self::getApiId();
    $application_secret = self::getApiSecret();
    $args = array();
    parse_str(trim($_COOKIE['fbs_' . $app_id], '\\"'), $args);
    ksort($args);
    $payload = '';
    foreach ($args as $key => $value)
    {
      if ($key != 'sig')
      {
        $payload .= $key . '=' . $value;
      }
    }
    if (md5($payload . $application_secret) != $args['sig'])
    {
      return null;
    }

    return $args;
  }

  /**
   * gets the facebook client instance
   *
   * @return Facebook
   * @author fabriceb
   * @since 2009-05-17
   * @since 2010-05-12 Benjamin Grandfond <benjaming@theodo.fr>: new Facebook php-sdk
   * @since 2010-09-03 Benjamin Grandfond : correct the parameters sent to the facebook class constructor
   */

  public static function getFacebookCookie()
  {
    $app_id = self::getApiKey();
    $application_secret = self::getApiSecret();
    $args = array();
    if (!isset($_COOKIE['fbs_' . $app_id]))
    {
      return null;
    }
    parse_str(trim($_COOKIE['fbs_' . $app_id], '\\"'), $args);
    ksort($args);
    $payload = '';
    foreach ($args as $key => $value)
    {
      if ($key != 'sig')
      {
        $payload .= $key . '=' . $value;
      }
    }
    if (md5($payload . $application_secret) != $args['sig'])
    {
      return null;
    }
    return $args;
  }

  public static function getFacebookClient()
  {
    if (self::$client === null)
    {
      $params = array(
        'appId'  => self::getApiId(),
        'secret' => self::getApiSecret(),
        'cookie' => self::getApiCookie(),
        'domain' => self::getApiDomain()
      );

      self::$client = new Facebook($params);
    }

    if (!self::$client)
    {
      error_log('Could not create facebook client.');
    }

    return self::$client;
  }

  /**
   * get the facebook session
   *
   * @return Array
   * @author Benjamin Grandfond <benjaming@theodo.fr>
   * @since 2010-05-13
   */
  public static function getSession()
  {

    return self::getFacebookClient()->getSession();
  }

  /**
   * get the facebook user
   *
   * @return Array
   * @author Benjamin Grandfond <benjaming@theodo.fr>
   * @since 2010-05-13
   */
  public static function getUser()
  {

    return self::getFacebookClient()->getUser();
  }

  /**
   * get datas through the faceook graph api
   * @see http://developers.facebook.com/docs/api
   *
   * @return Array
   * @author Benjamin Grandfond <benjaming@theodo.fr>
   * @since 2010-05-13
   */
  public static function getFacebookApi($param)
  {

    return self::getFacebookClient()->api($param);
  }

  /**
   * gets the facebook api key
   *
   * @return Facebook
   * @author fabriceb
   * @since 2009-05-17
   */
  public static function getApiKey()
  {

    return sfConfig::get('app_facebook_api_key');
  }

  /**
   * gets the facebook api secret
   *
   * @return Facebook
   * @author fabriceb
   * @since 2009-05-17
   */
  public static function getApiSecret()
  {

    return sfConfig::get('app_facebook_api_secret');
  }

  /**
   * get the facebook app id
   *
   * @return integer
   * @author Benjamin Grandfond <benjaming@theodo.fr>
   * @since 2010-09-03
   */
  public static function getApiId()
  {

    return sfConfig::get('app_facebook_api_id');
  }

  /**
   * get the facebook app cookie support
   *
   * @return Boolean
   * @author Benjamin Grandfond <benjaming@theodo.fr>
   * @since 2010-05-12
   */
  public static function getApiCookie()
  {

    return sfConfig::get('app_facebook_api_cookie', true);
  }

  /**
   * get the facebook app domain
   *
   * @return Boolean
   * @author Benjamin Grandfond <benjaming@theodo.fr>
   * @since 2010-05-12
   */
  public static function getApiDomain()
  {

    return sfConfig::get('app_facebook_api_domain', null);
  }

  /**
   * gets or create user with facebook uid inprofile
   *
   * @param Integer $facebook_uid
   * @param boolean $isActive
   * @return sfGuardUser $sfGuardUser
   */
  public static function getOrCreateUserByFacebookUid($facebook_uid, $isActive = true)
  {
    $sfGuardUser = self::getGuardAdapter()->getSfGuardUserByFacebookUid($facebook_uid, $isActive);

    if (!$sfGuardUser instanceof sfGuardUser)
    {
      if (sfConfig::get('sf_logging_enabled'))
      {
        sfContext::getInstance()->getLogger()->info('{sfFacebookConnect} No user exists with current email hash');
      }
      $sfGuardUser = self::getGuardAdapter()->createSfGuardUserWithFacebookUid($facebook_uid);
    }

    return $sfGuardUser;
  }

  /**
   * gets user with facebook uid inprofile
   *
   * @param Integer $facebook_uid
   * @param boolean $isActive
   * @return sfGuardUser $sfGuardUser
   */
  public static function getUserByFacebookUid($facebook_uid, $isActive = true)
  {
    $sfGuardUser = self::getGuardAdapter()->retrieveSfGuardUserByFacebookUid($facebook_uid, $isActive);

    if (!$sfGuardUser instanceof sfGuardUser)
    {
      if (sfConfig::get('sf_logging_enabled'))
      {
        sfContext::getInstance()->getLogger()->info('{sfFacebookConnect} No user exists with current email hash');
      }
    }

    return $sfGuardUser;
  }

  /**
   * Gets the currently logged sfGuardUser using Facebook Session
   *
   * @param boolean $create whether to automatically create a sfGuardUser
   * if none found corresponding to the Facebook session
   * @param boolean $isActive
   * @return sfGuardUser
   * @author fabriceb
   * @since 2009-05-17
   * @since 2009-08-25
   */
  public static function getSfGuardUserByFacebookSession($create = true, $isActive = true)
  {
    // We get the facebook uid from session
    $fb_uid = self::getAnyFacebookUid();
    if ($fb_uid)
    {

      if ($create)
      {

        return self::getOrCreateUserByFacebookUid($fb_uid, $isActive);
      }
      else
      {

        return self::getUserByFacebookUid($fb_uid, $isActive);
      }
    }

    if (sfConfig::get('sf_logging_enabled'))
    {
      sfContext::getInstance()->getLogger()->info('{sfFacebookConnect} No current Facebook session');
    }

    return null;
  }

  /**
   * checks the existence of the HTTP_X_FB_USER_REMOTE_ADDR porperty in the header
   * which is a sign of being included by the fbml interface
   *
   * @return boolean
   * @author fabriceb
   * @since Jun 8, 2009 fabriceb
   */
  public static function isInsideFacebook()
  {

    return isset($_SERVER['HTTP_X_FB_USER_REMOTE_ADDR']);
  }

  /**
   *
   * @return boolean
   * @author fabriceb
   * @since Jun 8, 2009 fabriceb
   */
  public static function inCanvas()
  {

    return self::getFacebookClient()->in_fb_canvas();
  }

  /**
   * redirects to the login page of the Facebook application if not logged yet
   *
   * @author fabriceb
   * @since Jun 8, 2009 fabriceb
   */
  public static function requireLogin()
  {
    self::getFacebookClient()->require_login();
  }

  /**
   * redirects depnding on in canvas or not
   *
   * @param $url
   * @param $statusCode
   * @return mixed sfView::NONE or sfStopException
   * @author fabriceb
   * @since Jun 8, 2009 fabriceb
   */
  public static function redirect($url, $statusCode = 302)
  {

    $context = sfContext::getInstance();
    $response = $context->getResponse();

    if (self::inCanvas())
    {
      $url = sfConfig::get('app_facebook_app_url').$context->getController()->genUrl($url, false);

      $text = '<fb:redirect url="' . $url . '"/>';
      $response->setContent(sfContext::getInstance()->getResponse()->getContent().$text);

      return sfView::NONE;
    }
    else
    {
      $fb_parameters = '?'.sfFacebook::getFacebookSigParameters(sfContext::getInstance()->getRequest());
      $url = $context->getController()->genUrl($url, true).$fb_parameters;

      $response->clearHttpHeaders();
      $response->setStatusCode($statusCode);
      $response->setHttpHeader('Location', $url);
      $response->setContent(sprintf('<meta http-equiv="refresh" content="%d;url=%s"/>', 0, htmlspecialchars($url, ENT_QUOTES, sfConfig::get('sf_charset'))));
      $response->send();

      throw new sfStopException();
    }
  }

  /**
   *
   * @param integer $user_uid
   * @return integer[]
   * @author fabriceb
   * @since Jun 9, 2009 fabriceb
   */
  public static function getFacebookFriendsUids($user_uid = null)
  {

    try
    {
      $friends_uids = self::getFacebookApi()->friends_get(null, $user_uid);
    }
    catch(FacebookRestClientException $e)
    {
      $friends_uids = array();
      if (sfConfig::get('sf_logging_enabled'))
      {
        sfContext::getInstance()->getLogger()->info('{FacebookRestClientException} '.$e->getMessage());
      }
    }

    return $friends_uids;
  }

  /**
  *
  * @return sfFacebookGuardAdapter
  * @author fabriceb
  * @since Aug 10, 2009
  * @since 2009-10-08 Alban Creton : added configurability of the Guard Adapter.
  */
  public static function getGuardAdapter()
  {
    if (self::$guard_adapter === null)
    {
      if(sfConfig::get('app_facebook_guard_adapter') && class_exists(sfConfig::get('app_facebook_guard_adapter'), true))
      {
        $class = sfConfig::get('app_facebook_guard_adapter');
      }
      else if (class_exists('sfGuardUserPeer', true))
      {
        $class = 'sfFacebookPropelGuardAdapter';
      }
      else
      {
        $class = 'sfFacebookDoctrineGuardAdapter';
      }
      self::$guard_adapter = new $class();
    }
    if (!self::$guard_adapter)
    {
      error_log('Could not create guard adapter.');
    }

    return self::$guard_adapter;
  }

  /**
   *
   * @return boolean
   * @author fabriceb
   * @since Aug 27, 2009
   */
  public static function isJsLoaded()
  {

    return self::$is_js_loaded;
  }

  /**
   *
   * @return void
   * @author fabriceb
   * @since Aug 27, 2009
   */
  public static function setJsLoaded()
  {
    self::$is_js_loaded = true;
  }

  /**
   * Dirty way to convert fr into fr_FR
   * @param string $culture
   * @return string
   * @author fabriceb
   * @since Aug 28, 2009
   */
  public static function getLocale($culture = null)
  {
    if (is_null($culture))
    {
      $culture = sfContext::getInstance()->getUser()->getCulture();
    }

    $culture_to_locale = array(
      'fr' => 'fr_FR',
      'en' => 'en_US',
      'de' => 'de_DE',
      'it' => 'it_IT',
    );

    return array_key_exists($culture, $culture_to_locale) ? $culture_to_locale[$culture] : $culture;
  }

  /**
  * @return interger $facebook_uid
  * @author fabriceb
  * @since Oct 6, 2009
  */
  public static function getAnyFacebookUid()
  {

    $cookie = self::getFacebookCookie();
    $fb_uid = $cookie['uid'];
    sfContext::getInstance()->getLogger()->info('{sfFacebookConnect} Fb_uid from cookie : '.$fb_uid);

    return $fb_uid;
  }

  /**
   *
   * @param sfWebRequest $request
   * @return string
   * @author fabriceb
   * @since Oct 12, 2009
   */
  public static function getFacebookSigParameters(sfWebRequest $request)
  {
    $parameters = $request->getParameterHolder()->getAll();

    $parameter_string = '';
    foreach ($parameters as $key => $parameter)
    {
      if (substr($key,0,3)=='fb_')
      {
        $parameter_string .= '&'.$key.'='.$parameter;
      }
    }

    return $parameter_string;
  }

}


Support for regexes, clearer logging, handle missing user_info

#!/usr/bin/python2.7
# encoding: utf-8

"""
Usage: %prog

Monitor system event notifications

Configuration:

The configuration file is divided into sections for each class of
events. Each section is a dictionary using the event condition as the
key ("NSWorkspaceDidWakeNotification", "State:/Network/Global/IPv4",
etc). Each event must have one of the following properties:

command:      a shell command
function:     the name of a python function
class:        the name of a python class which will be instantiated once
              and have methods called as events occur.
method:       (class, method) tuple
"""

from Cocoa import \
    CFAbsoluteTimeGetCurrent, \
    CFRunLoopAddSource, \
    CFRunLoopAddTimer, \
    CFRunLoopTimerCreate, \
    NSObject, \
    NSRunLoop, \
    NSWorkspace, \
    kCFRunLoopCommonModes

from SystemConfiguration import \
    SCDynamicStoreCopyKeyList, \
    SCDynamicStoreCreate, \
    SCDynamicStoreCreateRunLoopSource, \
    SCDynamicStoreSetNotificationKeys

from FSEvents import \
    FSEventStreamCreate, \
    FSEventStreamStart, \
    FSEventStreamScheduleWithRunLoop, \
    kFSEventStreamEventIdSinceNow, \
    kCFRunLoopDefaultMode, \
    kFSEventStreamEventFlagMustScanSubDirs, \
    kFSEventStreamEventFlagUserDropped, \
    kFSEventStreamEventFlagKernelDropped

import os
import os.path
import logging
import logging.handlers
import sys
import re
from subprocess import call
from optparse import OptionParser
from plistlib import readPlist, writePlist
from PyObjCTools import AppHelper
from functools import partial
import signal
from datetime import datetime
from objc import super

VERSION          = '$Revision: #4 $'

HANDLER_OBJECTS  = dict()     # Events which have a "class" handler use an instantiated object; we want to load only one copy
SC_HANDLERS      = dict()     # Callbacks indexed by SystemConfiguration keys
FS_WATCHED_FILES = dict()     # Callbacks indexed by filesystem path
WORKSPACE_HANDLERS = dict()   # handlers for workspace events

class BaseHandler(object):
    # pylint: disable-msg=C0111,R0903
    pass

class NotificationHandler(NSObject):
    """Simple base class for handling NSNotification events"""
    # Method names and class structure are dictated by Cocoa & PyObjC, which
    # is substantially different from PEP-8:
    # pylint: disable-msg=C0103,W0232,R0903

    def init(self):
        """NSObject-compatible initializer"""
        self = super(NotificationHandler, self).init()
        if self is None: return None
        self.callable = self.not_implemented
        return self # NOTE: Unlike Python, NSObject's init() must return self!

    def not_implemented(self, *args, **kwargs):
        """A dummy function which exists only to catch configuration errors"""
        # TODO: Is there a better way to report the caller's location?
        import inspect
        stack = inspect.stack()
        my_name = stack[0][3]
        caller  = stack[1][3]
        raise NotImplementedError(
            "%s should have been overridden. Called by %s as: %s(%s)" % (
                my_name,
                caller,
                my_name,
                ", ".join(map(repr, args) + [ "%s=%s" % (k, repr(v)) for k,v in kwargs.items() ])
            )
        )

    def onNotification_(self, the_notification):
        """Pass an NSNotifications to our handler"""
        if the_notification.userInfo:
            user_info = the_notification.userInfo()
        else:
            user_info = None
        self.callable(user_info=user_info) # pylint: disable-msg=E1101

def log_list(msg, items, level=logging.INFO):
    """
    Record a a list of values with a message

    This would ordinarily be a simple logging call but we want to keep the
    length below the 1024-byte syslog() limitation and we'll format things
    nicely by repeating our message with as many of the values as will fit.

    Individual items longer than the maximum length will be truncated.
    """

    max_len    = 1024 - len(msg % "")
    cur_len    = 0
    cur_items  = list()

    while [ i[:max_len] for i in items]:
        i = items.pop()
        if cur_len + len(i) + 2 > max_len:
            logging.info(msg % ", ".join(cur_items))
            cur_len = 0
            cur_items = list()

        cur_items.append(i)
        cur_len += len(i) + 2

    logging.log(level, msg % ", ".join(cur_items))

def get_callable_for_event(name, event_config, context=None):
    """
        Returns a callable object which can be used as a callback for any
        event. The returned function has context information, logging, etc.
        included so they do not need to be passed when the actual event
        occurs.

        NOTE: This function does not process "class" handlers - by design they
        are passed to the system libraries which expect a delegate object with
        various event handling methods
    """

    kwargs = {
        'context':  context,
        'key':      name,
        'config':   event_config,
    }

    if "command" in event_config:
        f = partial(do_shell, event_config["command"], **kwargs)
    elif "function" in event_config:
        f = partial(get_callable_from_string(event_config["function"]), **kwargs)
    elif "method" in event_config:
        f = partial(getattr(get_handler_object(event_config['method'][0]), event_config['method'][1]), **kwargs)
    else:
        raise AttributeError("%s have a class, method, function or command" % name)

    return f

def get_mod_func(callback):
    """Convert a fully-qualified module.function name to (module, function) - stolen from Django"""
    try:
        dot = callback.rindex('.')
    except ValueError:
        return (callback, '')
    return (callback[:dot], callback[dot+1:])

def get_callable_from_string(f_name):
    """Takes a string containing a function name (optionally module qualified) and returns a callable object"""
    try:
        mod_name, func_name = get_mod_func(f_name)
        if mod_name == "" and func_name == "":
            raise AttributeError("%s couldn't be converted to a module or function name" % f_name)

        module = __import__(mod_name)

        if func_name == "":
            func_name = mod_name # The common case is an eponymous class

        return getattr(module, func_name)

    except (ImportError, AttributeError), exc:
        raise RuntimeError("Unable to create a callable object for '%s': %s" % (f_name, exc))

def get_handler_object(class_name):
    """Return a single instance of the given class name, instantiating it if necessary"""

    if class_name not in HANDLER_OBJECTS:
        h_obj = get_callable_from_string(class_name)()
        if isinstance(h_obj, BaseHandler):
            pass # TODO: Do we even need BaseHandler any more?
        HANDLER_OBJECTS[class_name] = h_obj

    return HANDLER_OBJECTS[class_name]

def handle_sc_event(store, changed_keys, info):
    """Fire every event handler for one or more events"""

    for key in changed_keys:
        SC_HANDLERS[key](key=key, info=info)

def list_events(option, opt_str, value, parser):
    """Displays the list of events which can be monitored on the current system"""

    print 'On this system SystemConfiguration supports these events:'
    for event in sorted(SCDynamicStoreCopyKeyList(get_sc_store(), '.*')):
        print "\t", event

    print
    print "Standard NSWorkspace Notification messages:\n\t",
    print "\n\t".join('''
        NSWorkspaceDidLaunchApplicationNotification
        NSWorkspaceDidMountNotification
        NSWorkspaceDidPerformFileOperationNotification
        NSWorkspaceDidTerminateApplicationNotification
        NSWorkspaceDidUnmountNotification
        NSWorkspaceDidWakeNotification
        NSWorkspaceSessionDidBecomeActiveNotification
        NSWorkspaceSessionDidResignActiveNotification
        NSWorkspaceWillLaunchApplicationNotification
        NSWorkspaceWillPowerOffNotification
        NSWorkspaceWillSleepNotification
        NSWorkspaceWillUnmountNotification
    '''.split())

    sys.exit(0)

def process_commandline():
    """
        Process command-line options
        Load our preference file
        Configure the module path to add Application Support directories
    """
    parser          = OptionParser(__doc__.strip())
    support_path    = '/Library/' if os.getuid() == 0 else os.path.expanduser('~/Library/')
    preference_file = os.path.join(support_path, 'Preferences', 'com.googlecode.pymacadmin.crankd.plist')
    module_path     = os.path.join(support_path, 'Application Support/crankd')

    if os.path.exists(module_path):
        sys.path.append(module_path)
    else:
        print >> sys.stderr, "Module directory %s does not exist: Python handlers will need to use absolute pathnames" % module_path

    parser.add_option("-f", "--config", dest="config_file", help='Use an alternate config file instead of %default', default=preference_file)
    parser.add_option("-l", "--list-events", action="callback", callback=list_events, help="List the events which can be monitored")
    parser.add_option("-d", "--debug", action="count", default=False, help="Log detailed progress information")
    (options, args) = parser.parse_args()

    if len(args):
        parser.error("Unknown command-line arguments: %s" % args)

    options.support_path = support_path
    options.config_file = os.path.realpath(options.config_file)

    # This is somewhat messy but we want to alter the command-line to use full
    # file paths in case someone's code changes the current directory or the
    sys.argv = [ os.path.realpath(sys.argv[0]), ]

    if options.debug:
        logging.getLogger().setLevel(logging.DEBUG)
        sys.argv.append("--debug")

    if options.config_file:
        sys.argv.append("--config")
        sys.argv.append(options.config_file)

    return options

def load_config(options):
    """Load our configuration from plist or create a default file if none exists"""
    if not os.path.exists(options.config_file):
        logging.info("%s does not exist - initializing with an example configuration" % CRANKD_OPTIONS.config_file)
        print >>sys.stderr, 'Creating %s with default options for you to customize' % options.config_file
        print >>sys.stderr, '%s --list-events will list the events you can monitor on this system' % sys.argv[0]
        example_config = {
            'SystemConfiguration': {
                'State:/Network/Global/IPv4': {
                    'command': '/bin/echo "Global IPv4 config changed"'
                }
            },
            'NSWorkspace': {
                'NSWorkspaceDidMountNotification': {
                    'command': '/bin/echo "A new volume was mounted!"'
                },
                'NSWorkspaceDidWakeNotification': {
                    'command': '/bin/echo "The system woke from sleep!"'
                },
                'NSWorkspaceWillSleepNotification': {
                    'command': '/bin/echo "The system is about to go to sleep!"'
                }
            }
        }
        writePlist(example_config, options.config_file)
        sys.exit(1)

    logging.info("Loading configuration from %s" % CRANKD_OPTIONS.config_file)

    plist = readPlist(options.config_file)

    if "imports" in plist:
        for module in plist['imports']:
            try:
                __import__(module)
            except ImportError, exc:
                print >> sys.stderr, "Unable to import %s: %s" % (module, exc)
                sys.exit(1)
    return plist

def configure_logging():
    """Configures the logging module"""
    logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')

    # Enable logging to syslog as well:
    # Normally this would not be necessary but logging assumes syslog listens on
    # localhost syslog/udp, which is disabled on 10.5 (rdar://5871746)
    syslog = logging.handlers.SysLogHandler('/var/run/syslog')
    syslog.setFormatter(logging.Formatter('%(name)s: %(message)s'))
    syslog.setLevel(logging.INFO)
    logging.getLogger().addHandler(syslog)

def get_sc_store():
    """Returns an SCDynamicStore instance"""
    return SCDynamicStoreCreate(None, "crankd", handle_sc_event, None)

def add_workspace_notifications(nsw_config):
    # See http://developer.apple.com/documentation/Cocoa/Conceptual/Workspace/Workspace.html
    notification_center = NSWorkspace.sharedWorkspace().notificationCenter()

    for event in nsw_config:
        event_config = nsw_config[event]

        if "class" in event_config:
            obj         = get_handler_object(event_config['class'])
            objc_method = "on%s:" % event
            py_method   = objc_method.replace(":", "_")
            if not hasattr(obj, py_method) or not callable(getattr(obj, py_method)):
                print  >> sys.stderr, \
                    "NSWorkspace Notification %s: handler class %s must define a %s method" % (event, event_config['class'], py_method)
                sys.exit(1)

            notification_center.addObserver_selector_name_object_(obj, objc_method, event, None)
        else:
            handler          = NotificationHandler.new()
            handler.name     = "NSWorkspace Notification %s" % event
            handler.callable = get_callable_for_event(event, event_config, context=handler.name)

            assert(callable(handler.onNotification_))

            notification_center.addObserver_selector_name_object_(handler, "onNotification:", event, None)
            WORKSPACE_HANDLERS[event] = handler

    log_list("Listening for these NSWorkspace notifications: %s", nsw_config.keys())

def add_sc_notifications(sc_config):
    """
    This uses the SystemConfiguration framework to get a SCDynamicStore session
    and register for certain events. See the Apple SystemConfiguration
    documentation for details:

    <http://developer.apple.com/documentation/Networking/Reference/SysConfig/SCDynamicStore/CompositePage.html>

    TN1145 may also be of interest:
        <http://developer.apple.com/technotes/tn/tn1145.html>

    Inspired by the PyObjC SystemConfiguration callback demos:
    <https://svn.red-bean.com/pyobjc/trunk/pyobjc/pyobjc-framework-SystemConfiguration/Examples/CallbackDemo/>
    """

    keys = sc_config.keys()

    try:
        for key in keys:
            SC_HANDLERS[key] = get_callable_for_event(key, sc_config[key], context="SystemConfiguration: %s" % key)
    except AttributeError, exc:
        print  >> sys.stderr, "Error configuring SystemConfiguration events: %s" % exc
        sys.exit(1)

    store = get_sc_store()

    SCDynamicStoreSetNotificationKeys(store, None, keys)

    # Get a CFRunLoopSource for our store session and add it to the application's runloop:
    CFRunLoopAddSource(
        NSRunLoop.currentRunLoop().getCFRunLoop(),
        SCDynamicStoreCreateRunLoopSource(None, store, 0),
        kCFRunLoopCommonModes
    )

    log_list("Listening for these SystemConfiguration events: %s", keys)

def add_fs_notifications(fs_config):
    for path in fs_config:
        add_fs_notification(path, get_callable_for_event(path, fs_config[path], context="FSEvent: %s" % path))

def add_fs_notification(f_path, callback):
    """Adds an FSEvent notification for the specified path"""
    path = os.path.realpath(os.path.expanduser(f_path))
    if not os.path.exists(path):
        raise AttributeError("Cannot add an FSEvent notification: %s does not exist!" % path)

    if not os.path.isdir(path):
        path = os.path.dirname(path)

    try:
        FS_WATCHED_FILES[path].append(callback)
    except KeyError:
        FS_WATCHED_FILES[path] = [callback]

def start_fs_events():
    stream_ref = FSEventStreamCreate(
        None,                               # Use the default CFAllocator
        fsevent_callback,
        None,                               # We don't need a FSEventStreamContext
        FS_WATCHED_FILES.keys(),
        kFSEventStreamEventIdSinceNow,      # We only want events which happen in the future
        1.0,                                # Process events within 1 second
        0                                   # We don't need any special flags for our stream
    )

    if not stream_ref:
        raise RuntimeError("FSEventStreamCreate() failed!")

    FSEventStreamScheduleWithRunLoop(stream_ref, NSRunLoop.currentRunLoop().getCFRunLoop(), kCFRunLoopDefaultMode)

    if not FSEventStreamStart(stream_ref):
        raise RuntimeError("Unable to start FSEvent stream!")

    logging.debug("FSEventStream started for %d paths: %s" % (len(FS_WATCHED_FILES), ", ".join(FS_WATCHED_FILES)))

def fsevent_callback(stream_ref, full_path, event_count, paths, masks, ids):
    """Process an FSEvent (consult the Cocoa docs) and call each of our handlers which monitors that path or a parent"""
    for i in range(event_count):
        path = os.path.dirname(paths[i])

        if masks[i] & kFSEventStreamEventFlagMustScanSubDirs:
            recursive = True

        if masks[i] & kFSEventStreamEventFlagUserDropped:
            logging.error("We were too slow processing FSEvents and some events were dropped")
            recursive = True

        if masks[i] & kFSEventStreamEventFlagKernelDropped:
            logging.error("The kernel was too slow processing FSEvents and some events were dropped!")
            recursive = True
        else:
            recursive = False

        for i in [k for k in FS_WATCHED_FILES if path.startswith(k)]:
            logging.debug("FSEvent: %s: processing %d callback(s) for path %s" % (i, len(FS_WATCHED_FILES[i]), path))
            for j in FS_WATCHED_FILES[i]:
                j(i, path=path, recursive=recursive)

def timer_callback(*args):
    """Handles the timer events which we use simply to have the runloop run regularly. Currently this logs a timestamp for debugging purposes"""
    logging.debug("timer callback at %s" % datetime.now())

def main():
    configure_logging()

    global CRANKD_OPTIONS, CRANKD_CONFIG
    CRANKD_OPTIONS = process_commandline()
    CRANKD_CONFIG  = load_config(CRANKD_OPTIONS)

    if "NSWorkspace" in CRANKD_CONFIG:
        add_workspace_notifications(CRANKD_CONFIG['NSWorkspace'])

    if "SystemConfiguration" in CRANKD_CONFIG:
        add_sc_notifications(CRANKD_CONFIG['SystemConfiguration'])

    if "FSEvents" in CRANKD_CONFIG:
        add_fs_notifications(CRANKD_CONFIG['FSEvents'])

    # We reuse our FSEvents code to watch for changes to our files and
    # restart if any of our libraries have been updated:
    add_conditional_restart(CRANKD_OPTIONS.config_file, "Configuration file %s changed" % CRANKD_OPTIONS.config_file)
    for m in filter(lambda i: i and hasattr(i, '__file__'), sys.modules.values()):
        if m.__name__ == "__main__":
            msg = "%s was updated" % m.__file__
        else:
            msg = "Module %s was updated" % m.__name__

        add_conditional_restart(m.__file__, msg)

    signal.signal(signal.SIGHUP, partial(restart, "SIGHUP received"))

    start_fs_events()

    # NOTE: This timer is basically a kludge around the fact that we can't reliably get
    #       signals or Control-C inside a runloop. This wakes us up often enough to
    #       appear tolerably responsive:
    CFRunLoopAddTimer(
        NSRunLoop.currentRunLoop().getCFRunLoop(),
        CFRunLoopTimerCreate(None, CFAbsoluteTimeGetCurrent(), 2.0, 0, 0, timer_callback, None),
        kCFRunLoopCommonModes
    )

    try:
        AppHelper.runConsoleEventLoop(installInterrupt=True)
    except KeyboardInterrupt:
        logging.info("KeyboardInterrupt received, exiting")

    sys.exit(0)

def create_env_name(name):
    """
    Converts input names into more traditional shell environment name style

    >>> create_env_name("NSApplicationBundleIdentifier")
    'NSAPPLICATION_BUNDLE_IDENTIFIER'
    >>> create_env_name("NSApplicationBundleIdentifier-1234$foobar!")
    'NSAPPLICATION_BUNDLE_IDENTIFIER_1234_FOOBAR'
    """
    new_name = re.sub(r'''(?<=[a-z])([A-Z])''', '_\\1', name)
    new_name = re.sub(r'\W+', '_', new_name)
    new_name = re.sub(r'_{2,}', '_', new_name)
    return new_name.upper().strip("_")

def do_shell(command, context=None, **kwargs):
    """Executes a shell command with logging"""
    logging.info("%s: executing %s" % (context, command))

    child_env = {'CRANKD_CONTEXT': context}

    # We'll pull a subset of the available information in for shell scripts.
    # Anyone who needs more will probably want to write a Python handler
    # instead so they can reuse things like our logger & config info and avoid
    # ordeals like associative arrays in Bash
    for k in [ 'info', 'key' ]:
        if k in kwargs and kwargs[k]:
            child_env['CRANKD_%s' % k.upper()] = str(kwargs[k])

    user_info = kwargs.get("user_info")
    if user_info:
        for k, v in user_info.items():
            child_env[create_env_name(k)] = str(v)

    try:
        rc = call(command, shell=True, env=child_env)
        if rc == 0:
            logging.debug("`%s` returned %d" % (command, rc))
        elif rc < 0:
            logging.error("`%s` was terminated by signal %d" % (command, -rc))
        else:
            logging.error("`%s` returned %d" % (command, rc))
    except OSError, exc:
        logging.error("Got an exception when executing %s:" % (command, exc))

def add_conditional_restart(file_name, reason):
    """FSEvents monitors directories, not files. This function uses stat to
    restart only if the file's mtime has changed"""
    file_name = os.path.realpath(file_name)
    while not os.path.exists(file_name):
        file_name = os.path.dirname(file_name)
    orig_stat = os.stat(file_name).st_mtime

    def cond_restart(*args, **kwargs):
        try:
            if os.stat(file_name).st_mtime != orig_stat:
                restart(reason)
        except (OSError, IOError, RuntimeError), exc:
            restart("Exception while checking %s: %s" % (file_name, exc))

    add_fs_notification(file_name, cond_restart)

def restart(reason, *args, **kwargs):
    """Perform a complete restart of the current process using exec()"""
    logging.info("Restarting: %s" % reason)
    os.execv(sys.argv[0], sys.argv)

if __name__ == '__main__':
    main()

#!/usr/bin/python2.7
# encoding: utf-8

"""
Usage: %prog

Monitor system event notifications

Configuration:

The configuration file is divided into sections for each class of
events. Each section is a dictionary using the event condition as the
key ("NSWorkspaceDidWakeNotification", "State:/Network/Global/IPv4",
etc). Each event must have one of the following properties:

command:      a shell command
function:     the name of a python function
class:        the name of a python class which will be instantiated once
              and have methods called as events occur.
method:       (class, method) tuple
"""

from Cocoa import \
    CFAbsoluteTimeGetCurrent, \
    CFRunLoopAddSource, \
    CFRunLoopAddTimer, \
    CFRunLoopTimerCreate, \
    NSObject, \
    NSRunLoop, \
    NSWorkspace, \
    kCFRunLoopCommonModes

from SystemConfiguration import \
    SCDynamicStoreCopyKeyList, \
    SCDynamicStoreCreate, \
    SCDynamicStoreCreateRunLoopSource, \
    SCDynamicStoreSetNotificationKeys

from FSEvents import \
    FSEventStreamCreate, \
    FSEventStreamStart, \
    FSEventStreamScheduleWithRunLoop, \
    kFSEventStreamEventIdSinceNow, \
    kCFRunLoopDefaultMode, \
    kFSEventStreamEventFlagMustScanSubDirs, \
    kFSEventStreamEventFlagUserDropped, \
    kFSEventStreamEventFlagKernelDropped

import os
import os.path
import logging
import logging.handlers
import sys
import re
from subprocess import call
from optparse import OptionParser
from plistlib import readPlist, writePlist
from PyObjCTools import AppHelper
from functools import partial
import signal
from datetime import datetime
from objc import super

VERSION          = '$Revision: #4 $'

HANDLER_OBJECTS  = dict()     # Events which have a "class" handler use an instantiated object; we want to load only one copy
SC_HANDLERS      = dict()     # Callbacks indexed by SystemConfiguration keys
FS_WATCHED_FILES = dict()     # Callbacks indexed by filesystem path
WORKSPACE_HANDLERS = dict()   # handlers for workspace events

class BaseHandler(object):
    # pylint: disable-msg=C0111,R0903
    pass

class NotificationHandler(NSObject):
    """Simple base class for handling NSNotification events"""
    # Method names and class structure are dictated by Cocoa & PyObjC, which
    # is substantially different from PEP-8:
    # pylint: disable-msg=C0103,W0232,R0903

    def init(self):
        """NSObject-compatible initializer"""
        self = super(NotificationHandler, self).init()
        if self is None: return None
        self.callable = self.not_implemented
        return self # NOTE: Unlike Python, NSObject's init() must return self!

    def not_implemented(self, *args, **kwargs):
        """A dummy function which exists only to catch configuration errors"""
        # TODO: Is there a better way to report the caller's location?
        import inspect
        stack = inspect.stack()
        my_name = stack[0][3]
        caller  = stack[1][3]
        raise NotImplementedError(
            "%s should have been overridden. Called by %s as: %s(%s)" % (
                my_name,
                caller,
                my_name,
                ", ".join(map(repr, args) + [ "%s=%s" % (k, repr(v)) for k,v in kwargs.items() ])
            )
        )

    def onNotification_(self, the_notification):
        """Pass an NSNotifications to our handler"""
        if the_notification.userInfo:
            user_info = the_notification.userInfo()
        else:
            user_info = None
        self.callable(user_info=user_info) # pylint: disable-msg=E1101

def log_list(msg, items, level=logging.INFO):
    """
    Record a a list of values with a message

    This would ordinarily be a simple logging call but we want to keep the
    length below the 1024-byte syslog() limitation and we'll format things
    nicely by repeating our message with as many of the values as will fit.

    Individual items longer than the maximum length will be truncated.
    """

    max_len    = 1024 - len(msg % "")
    cur_len    = 0
    cur_items  = list()

    while [ i[:max_len] for i in items]:
        i = items.pop()
        if cur_len + len(i) + 2 > max_len:
            logging.info(msg % ", ".join(cur_items))
            cur_len = 0
            cur_items = list()

        cur_items.append(i)
        cur_len += len(i) + 2

    logging.log(level, msg % ", ".join(cur_items))

def get_callable_for_event(name, event_config, context=None):
    """
        Returns a callable object which can be used as a callback for any
        event. The returned function has context information, logging, etc.
        included so they do not need to be passed when the actual event
        occurs.

        NOTE: This function does not process "class" handlers - by design they
        are passed to the system libraries which expect a delegate object with
        various event handling methods
    """

    kwargs = {
        'context':  context,
        'key':      name,
        'config':   event_config,
    }

    if "command" in event_config:
        f = partial(do_shell, event_config["command"], **kwargs)
    elif "function" in event_config:
        f = partial(get_callable_from_string(event_config["function"]), **kwargs)
    elif "method" in event_config:
        f = partial(getattr(get_handler_object(event_config['method'][0]), event_config['method'][1]), **kwargs)
    else:
        raise AttributeError("%s have a class, method, function or command" % name)

    return f

def get_mod_func(callback):
    """Convert a fully-qualified module.function name to (module, function) - stolen from Django"""
    try:
        dot = callback.rindex('.')
    except ValueError:
        return (callback, '')
    return (callback[:dot], callback[dot+1:])

def get_callable_from_string(f_name):
    """Takes a string containing a function name (optionally module qualified) and returns a callable object"""
    try:
        mod_name, func_name = get_mod_func(f_name)
        if mod_name == "" and func_name == "":
            raise AttributeError("%s couldn't be converted to a module or function name" % f_name)

        module = __import__(mod_name)

        if func_name == "":
            func_name = mod_name # The common case is an eponymous class

        return getattr(module, func_name)

    except (ImportError, AttributeError), exc:
        raise RuntimeError("Unable to create a callable object for '%s': %s" % (f_name, exc))

def get_handler_object(class_name):
    """Return a single instance of the given class name, instantiating it if necessary"""

    if class_name not in HANDLER_OBJECTS:
        h_obj = get_callable_from_string(class_name)()
        if isinstance(h_obj, BaseHandler):
            pass # TODO: Do we even need BaseHandler any more?
        HANDLER_OBJECTS[class_name] = h_obj

    return HANDLER_OBJECTS[class_name]

def handle_sc_event(store, changed_keys, info):
    """Fire every event handler for one or more events"""
    try:
        for key in changed_keys:
            SC_HANDLERS[key](key=key, info=info)
    except KeyError:
        # If there's no exact match, go through the list again assuming regex
        for key in changed_keys:
            for handler in SC_HANDLERS:
                if re.match(handler, key):
                    SC_HANDLERS[handler](key=key, info=info)

def list_events(option, opt_str, value, parser):
    """Displays the list of events which can be monitored on the current system"""

    print 'On this system SystemConfiguration supports these events:'
    for event in sorted(SCDynamicStoreCopyKeyList(get_sc_store(), '.*')):
        print "\t", event

    print
    print "Standard NSWorkspace Notification messages:\n\t",
    print "\n\t".join('''
        NSWorkspaceDidLaunchApplicationNotification
        NSWorkspaceDidMountNotification
        NSWorkspaceDidPerformFileOperationNotification
        NSWorkspaceDidTerminateApplicationNotification
        NSWorkspaceDidUnmountNotification
        NSWorkspaceDidWakeNotification
        NSWorkspaceSessionDidBecomeActiveNotification
        NSWorkspaceSessionDidResignActiveNotification
        NSWorkspaceWillLaunchApplicationNotification
        NSWorkspaceWillPowerOffNotification
        NSWorkspaceWillSleepNotification
        NSWorkspaceWillUnmountNotification
    '''.split())

    sys.exit(0)

def process_commandline():
    """
        Process command-line options
        Load our preference file
        Configure the module path to add Application Support directories
    """
    parser          = OptionParser(__doc__.strip())
    support_path    = '/Library/' if os.getuid() == 0 else os.path.expanduser('~/Library/')
    preference_file = os.path.join(support_path, 'Preferences', 'com.googlecode.pymacadmin.crankd.plist')
    module_path     = os.path.join(support_path, 'Application Support/crankd')

    if os.path.exists(module_path):
        sys.path.append(module_path)
    else:
        print >> sys.stderr, "Module directory %s does not exist: Python handlers will need to use absolute pathnames" % module_path

    parser.add_option("-f", "--config", dest="config_file", help='Use an alternate config file instead of %default', default=preference_file)
    parser.add_option("-l", "--list-events", action="callback", callback=list_events, help="List the events which can be monitored")
    parser.add_option("-d", "--debug", action="count", default=False, help="Log detailed progress information")
    (options, args) = parser.parse_args()

    if len(args):
        parser.error("Unknown command-line arguments: %s" % args)

    options.support_path = support_path
    options.config_file = os.path.realpath(options.config_file)

    # This is somewhat messy but we want to alter the command-line to use full
    # file paths in case someone's code changes the current directory or the
    sys.argv = [ os.path.realpath(sys.argv[0]), ]

    if options.debug:
        logging.getLogger().setLevel(logging.DEBUG)
        sys.argv.append("--debug")

    if options.config_file:
        sys.argv.append("--config")
        sys.argv.append(options.config_file)

    return options

def load_config(options):
    """Load our configuration from plist or create a default file if none exists"""
    if not os.path.exists(options.config_file):
        logging.info("%s does not exist - initializing with an example configuration" % CRANKD_OPTIONS.config_file)
        print >>sys.stderr, 'Creating %s with default options for you to customize' % options.config_file
        print >>sys.stderr, '%s --list-events will list the events you can monitor on this system' % sys.argv[0]
        example_config = {
            'SystemConfiguration': {
                'State:/Network/Global/IPv4': {
                    'command': '/bin/echo "Global IPv4 config changed"'
                }
            },
            'NSWorkspace': {
                'NSWorkspaceDidMountNotification': {
                    'command': '/bin/echo "A new volume was mounted!"'
                },
                'NSWorkspaceDidWakeNotification': {
                    'command': '/bin/echo "The system woke from sleep!"'
                },
                'NSWorkspaceWillSleepNotification': {
                    'command': '/bin/echo "The system is about to go to sleep!"'
                }
            }
        }
        writePlist(example_config, options.config_file)
        sys.exit(1)

    logging.info("Loading configuration from %s" % CRANKD_OPTIONS.config_file)

    plist = readPlist(options.config_file)

    if "imports" in plist:
        for module in plist['imports']:
            try:
                __import__(module)
            except ImportError, exc:
                print >> sys.stderr, "Unable to import %s: %s" % (module, exc)
                sys.exit(1)
    return plist

def configure_logging():
    """Configures the logging module"""
    logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')

    # Enable logging to syslog as well:
    # Normally this would not be necessary but logging assumes syslog listens on
    # localhost syslog/udp, which is disabled on 10.5 (rdar://5871746)
    syslog = logging.handlers.SysLogHandler('/var/run/syslog')
    syslog.setFormatter(logging.Formatter('%(pathname)s[%(process)d]:%(message)s'))
    syslog.setLevel(logging.INFO)
    logging.getLogger().addHandler(syslog)

def get_sc_store():
    """Returns an SCDynamicStore instance"""
    return SCDynamicStoreCreate(None, "crankd", handle_sc_event, None)

def add_workspace_notifications(nsw_config):
    # See http://developer.apple.com/documentation/Cocoa/Conceptual/Workspace/Workspace.html
    notification_center = NSWorkspace.sharedWorkspace().notificationCenter()

    for event in nsw_config:
        event_config = nsw_config[event]

        if "class" in event_config:
            obj         = get_handler_object(event_config['class'])
            objc_method = "on%s:" % event
            py_method   = objc_method.replace(":", "_")
            if not hasattr(obj, py_method) or not callable(getattr(obj, py_method)):
                print  >> sys.stderr, \
                    "NSWorkspace Notification %s: handler class %s must define a %s method" % (event, event_config['class'], py_method)
                sys.exit(1)

            notification_center.addObserver_selector_name_object_(obj, objc_method, event, None)
        else:
            handler          = NotificationHandler.new()
            handler.name     = "NSWorkspace Notification %s" % event
            handler.callable = get_callable_for_event(event, event_config, context=handler.name)

            assert(callable(handler.onNotification_))

            notification_center.addObserver_selector_name_object_(handler, "onNotification:", event, None)
            WORKSPACE_HANDLERS[event] = handler
    log_list("Listening for these NSWorkspace notifications: %s", nsw_config.keys())

def add_sc_notifications(sc_config):
    """
    This uses the SystemConfiguration framework to get a SCDynamicStore session
    and register for certain events. See the Apple SystemConfiguration
    documentation for details:

    <http://developer.apple.com/documentation/Networking/Reference/SysConfig/SCDynamicStore/CompositePage.html>

    TN1145 may also be of interest:
        <http://developer.apple.com/technotes/tn/tn1145.html>

    Inspired by the PyObjC SystemConfiguration callback demos:
    <https://svn.red-bean.com/pyobjc/trunk/pyobjc/pyobjc-framework-SystemConfiguration/Examples/CallbackDemo/>
    """

    keys = sc_config.keys()

    try:
        for key in keys:
            SC_HANDLERS[key] = get_callable_for_event(key, sc_config[key], context="SystemConfiguration: %s" % key)
    except AttributeError, exc:
        print  >> sys.stderr, "Error configuring SystemConfiguration events: %s" % exc
        sys.exit(1)

    store = get_sc_store()

    SCDynamicStoreSetNotificationKeys(store, None, keys)

    # Get a CFRunLoopSource for our store session and add it to the application's runloop:
    CFRunLoopAddSource(
        NSRunLoop.currentRunLoop().getCFRunLoop(),
        SCDynamicStoreCreateRunLoopSource(None, store, 0),
        kCFRunLoopCommonModes
    )

    log_list("Listening for these SystemConfiguration events: %s", keys)

def add_fs_notifications(fs_config):
    for path in fs_config:
        add_fs_notification(path, get_callable_for_event(path, fs_config[path], context="FSEvent: %s" % path))

def add_fs_notification(f_path, callback):
    """Adds an FSEvent notification for the specified path"""
    path = os.path.realpath(os.path.expanduser(f_path))
    if not os.path.exists(path):
        raise AttributeError("Cannot add an FSEvent notification: %s does not exist!" % path)

    if not os.path.isdir(path):
        path = os.path.dirname(path)

    try:
        FS_WATCHED_FILES[path].append(callback)
    except KeyError:
        FS_WATCHED_FILES[path] = [callback]

def start_fs_events():
    stream_ref = FSEventStreamCreate(
        None,                               # Use the default CFAllocator
        fsevent_callback,
        None,                               # We don't need a FSEventStreamContext
        FS_WATCHED_FILES.keys(),
        kFSEventStreamEventIdSinceNow,      # We only want events which happen in the future
        1.0,                                # Process events within 1 second
        0                                   # We don't need any special flags for our stream
    )

    if not stream_ref:
        raise RuntimeError("FSEventStreamCreate() failed!")

    FSEventStreamScheduleWithRunLoop(stream_ref, NSRunLoop.currentRunLoop().getCFRunLoop(), kCFRunLoopDefaultMode)

    if not FSEventStreamStart(stream_ref):
        raise RuntimeError("Unable to start FSEvent stream!")

    logging.debug("FSEventStream started for %d paths: %s" % (len(FS_WATCHED_FILES), ", ".join(FS_WATCHED_FILES)))

def fsevent_callback(stream_ref, full_path, event_count, paths, masks, ids):
    """Process an FSEvent (consult the Cocoa docs) and call each of our handlers which monitors that path or a parent"""
    for i in range(event_count):
        path = os.path.dirname(paths[i])

        if masks[i] & kFSEventStreamEventFlagMustScanSubDirs:
            recursive = True

        if masks[i] & kFSEventStreamEventFlagUserDropped:
            logging.error("We were too slow processing FSEvents and some events were dropped")
            recursive = True

        if masks[i] & kFSEventStreamEventFlagKernelDropped:
            logging.error("The kernel was too slow processing FSEvents and some events were dropped!")
            recursive = True
        else:
            recursive = False

        for i in [k for k in FS_WATCHED_FILES if path.startswith(k)]:
            logging.debug("FSEvent: %s: processing %d callback(s) for path %s" % (i, len(FS_WATCHED_FILES[i]), path))
            for j in FS_WATCHED_FILES[i]:
                j(i, path=path, recursive=recursive)

def timer_callback(*args):
    """Handles the timer events which we use simply to have the runloop run regularly. Currently this logs a timestamp for debugging purposes"""
    logging.debug("timer callback at %s" % datetime.now())

def main():
    configure_logging()

    global CRANKD_OPTIONS, CRANKD_CONFIG
    CRANKD_OPTIONS = process_commandline()
    CRANKD_CONFIG  = load_config(CRANKD_OPTIONS)

    if "NSWorkspace" in CRANKD_CONFIG:
        add_workspace_notifications(CRANKD_CONFIG['NSWorkspace'])

    if "SystemConfiguration" in CRANKD_CONFIG:
        add_sc_notifications(CRANKD_CONFIG['SystemConfiguration'])

    if "FSEvents" in CRANKD_CONFIG:
        add_fs_notifications(CRANKD_CONFIG['FSEvents'])

    # We reuse our FSEvents code to watch for changes to our files and
    # restart if any of our libraries have been updated:
    add_conditional_restart(CRANKD_OPTIONS.config_file, "Configuration file %s changed" % CRANKD_OPTIONS.config_file)
    for m in filter(lambda i: i and hasattr(i, '__file__'), sys.modules.values()):
        if m.__name__ == "__main__":
            msg = "%s was updated" % m.__file__
        else:
            msg = "Module %s was updated" % m.__name__

        add_conditional_restart(m.__file__, msg)

    signal.signal(signal.SIGHUP, partial(restart, "SIGHUP received"))

    start_fs_events()

    # NOTE: This timer is basically a kludge around the fact that we can't reliably get
    #       signals or Control-C inside a runloop. This wakes us up often enough to
    #       appear tolerably responsive:
    CFRunLoopAddTimer(
        NSRunLoop.currentRunLoop().getCFRunLoop(),
        CFRunLoopTimerCreate(None, CFAbsoluteTimeGetCurrent(), 2.0, 0, 0, timer_callback, None),
        kCFRunLoopCommonModes
    )

    try:
        AppHelper.runConsoleEventLoop(installInterrupt=True)
    except KeyboardInterrupt:
        logging.info("KeyboardInterrupt received, exiting")

    sys.exit(0)

def create_env_name(name):
    """
    Converts input names into more traditional shell environment name style

    >>> create_env_name("NSApplicationBundleIdentifier")
    'NSAPPLICATION_BUNDLE_IDENTIFIER'
    >>> create_env_name("NSApplicationBundleIdentifier-1234$foobar!")
    'NSAPPLICATION_BUNDLE_IDENTIFIER_1234_FOOBAR'
    """
    new_name = re.sub(r'''(?<=[a-z])([A-Z])''', '_\\1', name)
    new_name = re.sub(r'\W+', '_', new_name)
    new_name = re.sub(r'_{2,}', '_', new_name)
    return new_name.upper().strip("_")

def do_shell(command, context=None, **kwargs):
    """Executes a shell command with logging"""
    logging.info("%s: executing %s" % (context, command))

    child_env = {'CRANKD_CONTEXT': context}

    # We'll pull a subset of the available information in for shell scripts.
    # Anyone who needs more will probably want to write a Python handler
    # instead so they can reuse things like our logger & config info and avoid
    # ordeals like associative arrays in Bash
    for k in [ 'info', 'key' ]:
        if k in kwargs and kwargs[k]:
            child_env['CRANKD_%s' % k.upper()] = str(kwargs[k])

    if 'user_info' in kwargs:
        for k, v in kwargs['user_info'].items():
            child_env[create_env_name(k)] = str(v)

    try:
        rc = call(command, shell=True, env=child_env)
        if rc == 0:
            logging.debug("`%s` returned %d" % (command, rc))
        elif rc < 0:
            logging.error("`%s` was terminated by signal %d" % (command, -rc))
        else:
            logging.error("`%s` returned %d" % (command, rc))
    except OSError, exc:
        logging.error("Got an exception when executing %s:" % (command, exc))

def add_conditional_restart(file_name, reason):
    """FSEvents monitors directories, not files. This function uses stat to
    restart only if the file's mtime has changed"""
    file_name = os.path.realpath(file_name)
    while not os.path.exists(file_name):
        file_name = os.path.dirname(file_name)
    orig_stat = os.stat(file_name).st_mtime

    def cond_restart(*args, **kwargs):
        try:
            if os.stat(file_name).st_mtime != orig_stat:
                restart(reason)
        except (OSError, IOError, RuntimeError), exc:
            restart("Exception while checking %s: %s" % (file_name, exc))

    add_fs_notification(file_name, cond_restart)

def restart(reason, *args, **kwargs):
    """Perform a complete restart of the current process using exec()"""
    logging.info("Restarting: %s" % reason)
    os.execv(sys.argv[0], sys.argv)

if __name__ == '__main__':
    main()


lil spelling correction

<?php

	require_once(TOOLKIT . '/class.entrymanager.php');

	Class extension_root_page_params extends Extension{

		protected $section_data;
		protected $_page;
		protected $static_section_name;

		static $alreadyRan = false;

		public function about(){
			return array('name' => 'Root page params',
						 'version' => '1.2',
						 'release-date' => '2009-12-10',
						 'author' => array('name' => 'Huib Keemink',
										   'website' => 'http://www.creativedutchmen.com',
										   'email' => 'huib@creativedutchmen.com')
				 		);
		}

		public function getSubscribedDelegates(){
			return array(
						array(
							'page' => '/frontend/',
							'delegate' => 'FrontendPrePageResolve',
							'callback' => 'addPage'
						),
						array(
							'page' => '/system/preferences/',
							'delegate' => 'AddCustomPreferenceFieldsets',
							'callback' => 'append_preferences'
						),
						array(
							'page' => '/system/preferences/',
							'delegate' => 'Save',
							'callback' => 'save_settings'
						),
			);
		}

		public function addPage(&$context){

			//to prevent the callback loop
			if(!$this->alreadyRan){
				$this->alreadyRan = true;
				//the only way to access the current (active) pages.
				$front = FrontEnd::Page();

				if(!$front->resolvePage($context['page'])){
					//uses home page if no page is set in the config panel.
					if($this->_get_fallback() == ''){
						$indexPage = $this->__getIndexPage();
						$indexHandle = $indexPage['handle'];
					}
					else{
						$indexHandle = $this->_get_fallback();
					}

					//adds the home page to the handle, if the current page is not found.
					//requires the home page to fallback to a 404 if the params do not match, otherwise no 404 error will ever be created.

					$params = $context['page'];

					if($this->_Parent->Configuration->get('map_sub_to_front', 'maptofront') == 'no'){
						$tmp = substr($indexHandle,0, strrpos($indexHandle, '/'));
						if(strlen($tmp) > 0){
							$params = substr($context['page'], strpos($context['page'], $tmp)+strlen($tmp));
						}
						else{
							$params = '';
						}
					}
					$context['page'] = $indexHandle.$params;
				}
			}

		}

		public function append_preferences(&$context)
		{
			# Add new fieldset
			$group = new XMLElement('fieldset');
			$group->setAttribute('class', 'settings');
			$group->appendChild(new XMLElement('legend', 'Root Page Params'));

			# Add Site Reference field
			//$label = Widget::Label('Fallback page');
			//$label->appendChild(Widget::Input('settings[maptofront][fallback]', General::Sanitize($this->_get_fallback())));

			//try to add a select box for the page (more user friendly)
			$label = Widget::Label(__('Page to append parameters to'));

			$pages = $this->_Parent->Database->fetch("
				SELECT
					p.*
				FROM
					`tbl_pages` AS p
				WHERE
					p.id != '{mysql_real_escape_string($page_id)}'
				ORDER BY
					p.title ASC
			");

			$options = array(
				array('', false, '')
			);

			if (is_array($pages) && !empty($pages)) {
				if (!function_exists('__compare_pages')) {
					function __compare_pages($a, $b) {
						return strnatcasecmp($a[2], $b[2]);
					}
				}

				foreach ($pages as $page) {
					$options[] = array(
						$this->_Parent->resolvePagePath($page['id']), $this->_Parent->Configuration->get('fallback', 'maptofront') == $this->_Parent->resolvePagePath($page['id']),
						'/'.$this->_Parent->resolvePagePath($page['id'])
					);
				}

				usort($options, '__compare_pages');
			}

			$label->appendChild(Widget::Select(
				'settings[maptofront][fallback]', $options
			));

			$group->appendChild($label);
			$group->appendChild(new XMLElement('p', 'The page to append the parameters to. Leave empty for home (default).', array('class' => 'help')));

			$label = Widget::Label();
			$input = Widget::Input('settings[maptofront][map_sub_to_front]', 'yes', 'checkbox');
			if($this->_Parent->Configuration->get('map_sub_to_front', 'maptofront') == 'yes') $input->setAttribute('checked', 'checked');
			$label->setValue($input->generate() . ' ' . __('Map supages to home page'));

			$group->appendChild($label);
			$group->appendChild(new XMLElement('p', 'Maps subpages to the root page when checked, maps subpages to their parents if unchecked.', array('class' => 'help')));

			$context['wrapper']->appendChild($group);
		}

		//any way to get this without using the database?
		function __getIndexPage(){
			$row = $this->_Parent->Database->fetchRow(0, "SELECT `tbl_pages`.* FROM `tbl_pages`, `tbl_pages_types`
															  WHERE `tbl_pages_types`.page_id = `tbl_pages`.id
															  AND tbl_pages_types.`type` = 'index'
															  LIMIT 1");
			return $row;
		}

		function _get_fallback(){
			$default_fallback = '';
			$val = $this->_Parent->Configuration->get('fallback', 'maptofront');

			return (isset($val)) ? $val : $default_fallback;
		}

		function save_settings($context){
			if(!isset($context['settings']['maptofront']['map_sub_to_front'])) $context['settings']['maptofront']['map_sub_to_front'] = 'no';

			if(!isset($context['settings']['maptofront'])){
				$context['settings']['maptofront'] = array('map_sub_to_front' => 'no');
			}
		}
	}

<?php

	require_once(TOOLKIT . '/class.entrymanager.php');

	Class extension_root_page_params extends Extension{

		protected $section_data;
		protected $_page;
		protected $static_section_name;

		static $alreadyRan = false;

		public function about(){
			return array('name' => 'Root page params',
						 'version' => '1.2',
						 'release-date' => '2009-12-10',
						 'author' => array('name' => 'Huib Keemink',
										   'website' => 'http://www.creativedutchmen.com',
										   'email' => 'huib@creativedutchmen.com')
				 		);
		}

		public function getSubscribedDelegates(){
			return array(
						array(
							'page' => '/frontend/',
							'delegate' => 'FrontendPrePageResolve',
							'callback' => 'addPage'
						),
						array(
							'page' => '/system/preferences/',
							'delegate' => 'AddCustomPreferenceFieldsets',
							'callback' => 'append_preferences'
						),
						array(
							'page' => '/system/preferences/',
							'delegate' => 'Save',
							'callback' => 'save_settings'
						),
			);
		}

		public function addPage(&$context){

			//to prevent the callback loop
			if(!$this->alreadyRan){
				$this->alreadyRan = true;
				//the only way to access the current (active) pages.
				$front = FrontEnd::Page();

				if(!$front->resolvePage($context['page'])){
					//uses home page if no page is set in the config panel.
					if($this->_get_fallback() == ''){
						$indexPage = $this->__getIndexPage();
						$indexHandle = $indexPage['handle'];
					}
					else{
						$indexHandle = $this->_get_fallback();
					}

					//adds the home page to the handle, if the current page is not found.
					//requires the home page to fallback to a 404 if the params do not match, otherwise no 404 error will ever be created.

					$params = $context['page'];

					if($this->_Parent->Configuration->get('map_sub_to_front', 'maptofront') == 'no'){
						$tmp = substr($indexHandle,0, strrpos($indexHandle, '/'));
						if(strlen($tmp) > 0){
							$params = substr($context['page'], strpos($context['page'], $tmp)+strlen($tmp));
						}
						else{
							$params = '';
						}
					}
					$context['page'] = $indexHandle.$params;
				}
			}

		}

		public function append_preferences(&$context)
		{
			# Add new fieldset
			$group = new XMLElement('fieldset');
			$group->setAttribute('class', 'settings');
			$group->appendChild(new XMLElement('legend', 'Root Page Params'));

			# Add Site Reference field
			//$label = Widget::Label('Fallback page');
			//$label->appendChild(Widget::Input('settings[maptofront][fallback]', General::Sanitize($this->_get_fallback())));

			//try to add a select box for the page (more user friendly)
			$label = Widget::Label(__('Page to append parameters to'));

			$pages = $this->_Parent->Database->fetch("
				SELECT
					p.*
				FROM
					`tbl_pages` AS p
				WHERE
					p.id != '{mysql_real_escape_string($page_id)}'
				ORDER BY
					p.title ASC
			");

			$options = array(
				array('', false, '')
			);

			if (is_array($pages) && !empty($pages)) {
				if (!function_exists('__compare_pages')) {
					function __compare_pages($a, $b) {
						return strnatcasecmp($a[2], $b[2]);
					}
				}

				foreach ($pages as $page) {
					$options[] = array(
						$this->_Parent->resolvePagePath($page['id']), $this->_Parent->Configuration->get('fallback', 'maptofront') == $this->_Parent->resolvePagePath($page['id']),
						'/'.$this->_Parent->resolvePagePath($page['id'])
					);
				}

				usort($options, '__compare_pages');
			}

			$label->appendChild(Widget::Select(
				'settings[maptofront][fallback]', $options
			));

			$group->appendChild($label);
			$group->appendChild(new XMLElement('p', 'The page to append the parameters to. Leave empty for home (default).', array('class' => 'help')));

			$label = Widget::Label();
			$input = Widget::Input('settings[maptofront][map_sub_to_front]', 'yes', 'checkbox');
			if($this->_Parent->Configuration->get('map_sub_to_front', 'maptofront') == 'yes') $input->setAttribute('checked', 'checked');
			$label->setValue($input->generate() . ' ' . __('Map subpages to home page'));

			$group->appendChild($label);
			$group->appendChild(new XMLElement('p', 'Maps subpages to the root page when checked, maps subpages to their parents if unchecked.', array('class' => 'help')));

			$context['wrapper']->appendChild($group);
		}

		//any way to get this without using the database?
		function __getIndexPage(){
			$row = $this->_Parent->Database->fetchRow(0, "SELECT `tbl_pages`.* FROM `tbl_pages`, `tbl_pages_types`
															  WHERE `tbl_pages_types`.page_id = `tbl_pages`.id
															  AND tbl_pages_types.`type` = 'index'
															  LIMIT 1");
			return $row;
		}

		function _get_fallback(){
			$default_fallback = '';
			$val = $this->_Parent->Configuration->get('fallback', 'maptofront');

			return (isset($val)) ? $val : $default_fallback;
		}

		function save_settings($context){
			if(!isset($context['settings']['maptofront']['map_sub_to_front'])) $context['settings']['maptofront']['map_sub_to_front'] = 'no';

			if(!isset($context['settings']['maptofront'])){
				$context['settings']['maptofront'] = array('map_sub_to_front' => 'no');
			}
		}
	}


Mergebranch

#include "qmlunittestrunner.h"
#include "qmllogger.h"
#include <QDeclarativeContext>
#include <QDebug>
#include <QDir>

QmlUnitTestRunner::QmlUnitTestRunner(QApplication *app) :
    QObject(app)
{
    this->app = app;

    QStringList args = app->arguments();
    // Skips executable
    args.removeAt(0);

    if (args.count() == 0) {
        qDebug() << "No arguments passed, failing back to qmlunit Test Suite (" << (app->applicationDirPath() + "/test") << ")\n";
        args = QStringList(app->applicationDirPath() + "/test");
    }

    QStringListIterator i(args);
    while(i.hasNext())
        findTests(i.next());

    i = QStringListIterator(tests);
    qDebug() << "Tests files found:";
    while(i.hasNext()) {
        QString currentArg = i.next();
        qDebug() << "\t" << currentArg;
    }
}

void QmlUnitTestRunner::setup(){
    QmlLogger *logger = new QmlLogger(app->applicationDirPath(), this);

    QDeclarativeEngine *engine = logger->engine();

    engine->setOfflineStoragePath(QDir::currentPath() + "/storage");
    engine->addImportPath(QDir::currentPath() + "/QmlUnit");
    engine->rootContext()->setContextProperty("testsInput", tests);
    engine->rootContext()->setContextProperty("currentPath", QDir::currentPath());

    logger->setup();
}

int QmlUnitTestRunner::exec() {
    setup();
    return app->exec();
}

void QmlUnitTestRunner::findTests(QString path) {
    if (isTest(path)) {
        tests << QDir(path).absolutePath();
        return;
    }

    QStringList filters; filters << "*";
    QDir dir = QDir(QDir(path).absolutePath());

    QListIterator<QFileInfo> files(dir.entryInfoList(filters, QDir::AllEntries | QDir::NoDotAndDotDot));
    while(files.hasNext()) {
        QFileInfo file = files.next();
        if (file.fileName() == "." || file.fileName() == "..") continue;

        if (isTest(file))
            tests << file.absoluteFilePath();
        else if (isDir(file))
            findTests(file.absoluteFilePath());
    }
}

bool QmlUnitTestRunner::isTest(QFileInfo file){
    return isTest(file.fileName());
}

bool QmlUnitTestRunner::isTest(QString filePath){
    return filePath.endsWith("Test.qml");
}

bool QmlUnitTestRunner::isDir(QFileInfo file){
    return QDir(file.absoluteFilePath()).exists();
}

#include "qmlunittestrunner.h"
#include "qmllogger.h"
#include <QDeclarativeContext>
#include <QDebug>
#include <QDir>

QmlUnitTestRunner::QmlUnitTestRunner(QApplication *app) :
    QObject(app)
{
    this->app = app;

    QStringList args = app->arguments();
    // Skips executable
    args.removeAt(0);

    // Parse possible xmlUnitOutput arg
    bool xmlOutput = false;

    if ((args.count() > 0) && args.contains("-xml")) {
        xmlOutput = true;
        args.removeAt(args.indexOf("-xml"));
    }

    if (args.count() == 0) {
        // Surpress all non-xml output if xml output was requested
        if (!xmlOutput)
          qDebug() << "No arguments passed, failing back to qmlunit Test Suite (" << (app->applicationDirPath() + "/test") << ")\n";
        args = QStringList(app->applicationDirPath() + "/test");
    }

    QStringListIterator i(args);
    while(i.hasNext())
        findTests(i.next());

    i = QStringListIterator(tests);

    // Surpress all non-xml output if xml output was requested
    if (!xmlOutput) {
      qDebug() << "Tests files found:";
      while(i.hasNext()) {
          QString currentArg = i.next();
          qDebug() << "\t" << currentArg;
      }
    }
}

void QmlUnitTestRunner::setup(){
    QmlLogger *logger = new QmlLogger(app->applicationDirPath(), this);

    QDeclarativeEngine *engine = logger->engine();

    engine->setOfflineStoragePath(QDir::currentPath() + "/storage");
    engine->addImportPath(QDir::currentPath() + "/QmlUnit");
    engine->rootContext()->setContextProperty("testsInput", tests);
    engine->rootContext()->setContextProperty("currentPath", QDir::currentPath());

    logger->setup();
}

int QmlUnitTestRunner::exec() {
    setup();
    return app->exec();
}

void QmlUnitTestRunner::findTests(QString path) {
    if (isTest(path)) {
        tests << QDir(path).absolutePath();
        return;
    }

    QStringList filters; filters << "*";
    QDir dir = QDir(QDir(path).absolutePath());

    QListIterator<QFileInfo> files(dir.entryInfoList(filters, QDir::AllEntries | QDir::NoDotAndDotDot));
    while(files.hasNext()) {
        QFileInfo file = files.next();
        if (file.fileName() == "." || file.fileName() == "..") continue;

        if (isTest(file))
            tests << file.absoluteFilePath();
        else if (isDir(file))
            findTests(file.absoluteFilePath());
    }
}

bool QmlUnitTestRunner::isTest(QFileInfo file){
    return isTest(file.fileName());
}

bool QmlUnitTestRunner::isTest(QString filePath){
    return filePath.endsWith("Test.qml");
}

bool QmlUnitTestRunner::isDir(QFileInfo file){
    return QDir(file.absoluteFilePath()).exists();
}

var currentTestSuite;
var testSuites = [];

function addTestSuite(name) {

  currentTestSuite = {
    name: name,
    errors: 0,
    testsTotal: 0,
    time: 0.0,
    failures: 0,
    tests: []
  };
  testSuites.push(currentTestSuite)
}

function parseFailureMessage(assertions) {
  var message = ""
  for (var i=0; i<assertions.length; i++) {
    if (!assertions[i].result && assertions[i].message)
      message += assertions[i].message + "; "
  }

  return message
}

function addTestCase(testSuite, name, testsTotal, failures, assertions) {
  var testCase = {
    name: name,
    time: 0,
    testsTotal: testsTotal,
    failures: failures,
    failureMessage: parseFailureMessage(assertions)
  };

  testSuite.testsTotal += testsTotal
  testSuite.failures += failures

  testSuite.tests.push(testCase)
}

function printTestResultsInXUnitFormat(logger) {

  logger.log("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>")
  logger.log("<testsuites>")

  testSuites.forEach(function(testSuite) {
    logger.log("  <testsuite name=\"" + testSuite.name + "\" tests=\"" +testSuite.testsTotal +
                "\" failures=\"" + testSuite.failures + "\" errors=\"" + testSuite.errors +
                "\" time=\"" + testSuite.time + "\">")

    testSuite.tests.forEach(function(test) {
      if (test.failures == 0) {
        logger.log("    <testcase name=\"" + test.name + "\" time=\"" + test.time + "\"/>")
      }
      else {
        logger.log("    <testcase name=\"" + test.name + "\" time=\"" + test.time + "\">")
        logger.log("      <failure type=\"qmlunit test failure\" message=\"" + test.failureMessage + "\"/>")
        logger.log("    </testcase>")
      }
    });

    logger.log("  </testsuite>")
  });

  logger.log("</testsuites>")
}


fx-text for the pack

/*
---

name: Element.retype

description: Effect to animated replace the text of an element.

authors: Dipl.-Ing. (FH) André Fiedler <kontakt@visualdrugs.net>

copyright: Copyright (c) 2010 Dipl.-Ing. (FH) André Fiedler <kontakt@visualdrugs.net>

license: MIT-style license.

version: 1.0

requires: Fx.Text

provides: Element.retype

...
*/

Element.Properties.retype = {

    set: function(options){
        var retype = this.retrieve('retype');
        if (retype)
            retype.cancel();
        return this.eliminate('retype').store('retype:options', $extend({
            link: 'cancel'
        }, options));
    },

    get: function(options){
        if (options || !this.retrieve('retype')) {
            if (options || !this.retrieve('retype:options'))
                this.set('retype', options);
            this.store('retype', new Fx.Text(this, this.retrieve('retype:options')));
        }
        return this.retrieve('retype');
    }
};

Element.implement({

    retype: function(from, to){
        this.get('retype').start(from, to);
        return this;
    }
});

/*
---

name: Fx.Text

description: Effect to animated replace the text of an element.

authors: Dipl.-Ing. (FH) André Fiedler <kontakt@visualdrugs.net>

copyright: Copyright (c) 2010 Dipl.-Ing. (FH) André Fiedler <kontakt@visualdrugs.net>

license: MIT-style license.

version: 1.2.1

requires: Core/1.2.4: Fx

provides: Fx.Text

...
*/

Fx.Text = new Class({

    Extends: Fx,

    initialize: function(element, options){
        this.element = this.subject = document.id(element);
        this.parent(options);
    },

    set: function(now){
        this.element.set('text', now);
        return this;
    },

    step: function(){
        if (!this.to) {
            this.to = this.from;
            this.from = this.element.get('text', '');
        }
        return this.parent();
    },

    compute: function(from, to, delta){
        var l = Math.round(to.length * delta);
        var r = Math.round((from.length - to.length) * delta);
        return to.substr(0, l) + from.substr(l, from.length - l - r);
    }
});

/*
---

name: Element.retype

description: Effect to animated replace the text of an element.

authors: Dipl.-Ing. (FH) André Fiedler <kontakt@visualdrugs.net>

copyright: Copyright (c) 2010 Dipl.-Ing. (FH) André Fiedler <kontakt@visualdrugs.net>

license: MIT-style license.

version: 1.0

requires:
  - Fx.Text

provides: Element.retype

...
*/

Element.Properties.retype = {

	set: function(options){
		var retype = this.retrieve('retype');
		if (retype)
			retype.cancel();
		return this.eliminate('retype').store('retype:options', $extend({
			link: 'cancel'
		}, options));
	},

	get: function(options){
		if (options || !this.retrieve('retype')){
			if (options || !this.retrieve('retype:options'))
				this.set('retype', options);
			this.store('retype', new Fx.Text(this, this.retrieve('retype:options')));
		}
		return this.retrieve('retype');
	}

};

Element.implement({

	retype: function(from, to){
		this.get('retype').start(from, to);
		return this;
	}

});

/*
---

name: Fx.Text

description: Effect to animated replace the text of an element.

authors: Dipl.-Ing. (FH) André Fiedler <kontakt@visualdrugs.net>

copyright: Copyright (c) 2010 Dipl.-Ing. (FH) André Fiedler <kontakt@visualdrugs.net>

license: MIT-style license.

version: 1.2.1

requires:
  - Core/Element
  - Core/Fx

provides: Fx.Text

...
*/

Fx.Text = new Class({

	Extends: Fx,

	initialize: function(element, options){
		this.element = this.subject = document.id(element);
		this.parent(options);
	},

	set: function(now){
		this.element.set('text', now);
		return this;
	},

	step: function(){
		if (!this.to){
			this.to = this.from;
			this.from = this.element.get('text', '');
		}
		return this.parent();
	},

	compute: function(from, to, delta){
		var l = Math.round(to.length * delta),
			r = Math.round((from.length - to.length) * delta);
		return to.substr(0, l) + from.substr(l, from.length - l - r);
	}

});