<?php

/**
 * Danltn | http://danltn.com/
 * MSN: msn@danltn.com
 * Email: daniel@neville.tk
 * Started: 19/6/2008 16:01 (UK) 
 * Tested: No
 * PHP 4/5: 5
 * No warranty is given to code used
 */

/**
 * PingFM_Wrapper
 * 
 * @package   PingFM_Wrappers
 * @url       http://danltn.com
 * @desc      Package which allows easy interaction with the Ping.fm service
 * @author    Daniel Neville
 * @copyright 2008
 * @version   0.2b
 * @access    public
 * @license   GPL
 */
class PingFM_Wrapper
{
    /** Your API */
    public $dev_api;
    /** Users API */
    public $user_api;

    /** Actual reply code */
    public $reply;
    /** XML parsed with SimpleXML */
    public $xml;
    /** Details array returned, this is what you should use */
    public $details;

    /** Acceptable methods for API usage **/
    static public $methods = array( 'validate', 'services', 'triggers', 'latest', 'message', 'tpost', 'post' );
    /** Acceptable methods to use if we're using the post method **/
    static public $post_methods = array( 'blog', 'microblog', 'status' );

    /** All the URLs for different services **/
    const VALIDATE_URL = 'http://api.ping.fm/v1/user.validate';
    const SERVICES_URL = 'http://api.ping.fm/v1/user.services';
    const TRIGGERS_URL = 'http://api.ping.fm/v1/user.triggers';
    const MESSAGE_URL = 'http://api.ping.fm/v1/user.message';
    const LATEST_URL = 'http://api.ping.fm/v1/user.latest';
    const TPOST_URL = 'http://api.ping.fm/v1/user.tpost';
    const POST_URL = 'http://api.ping.fm/v1/user.post';

    /**
     * PingFM_Wrapper::__construct()
     * 
     * @param  mixed  $dev_api   Your developer API
     * @param  mixed  $user_api  The users API
     * @param  bool   $check     Should we run checks to make sure all the extensions are loaded?
     * @return void
     */
    public function __construct( $dev_api = null, $user_api = null, $check = true )
    {
        /** Construct class, runs on initialization **/
        if ( $check )
        {
            /** If the check parametre is set **/
            if ( !extension_loaded('curl') /** Is the cURL extension loaded? **/ )
            {
                throw new Exception( 'You don\'t have cURL loaded.' );
                /** If not, throw an exception **/
            }
            if ( !extension_loaded('SimpleXML') /** Is the SimpleXML extension loaded? **/ )
            {
                throw new Exception( 'You don\'t have the SimpleXML lib loaded.' );
                /** If not, throw an exception **/
            }
        }
        /** if $dev_api and $user_api aren't the default values of null, set them in the class. **/
        if ( !is_null($dev_api) ) $this->dev_api = $dev_api;
        if ( !is_null($user_api) ) $this->user_api = $user_api;
    }

    /**
     * PingFM_Wrapper::act()
     * 
     * @param string  $method             What we're carrying, this is taken care of with the PingFM_Functions extension
     * @param mixed   $extra_post_fields  Any extra fields required, these are taken care of with the PingFM_Functions extension
     * @return bool   $status 			  Whether the action completed successfully or not
     */
    public function act( $method = '', $extra_post_fields = array( /** You'll need to fill this in if you're using a method that needs more that just the API codes */ ) )
    {
        if ( !in_array($method, self::$methods) ) throw new Exception( 'The selected method: ' . $method . ' is not supported by Ping.fm' );
        /** Throw an error if the method isn't supported */
        $extra_post_fields = $this->_check_fields( $method, $extra_post_fields );
        /** Check all the fields are correct, if they're not fix them if we can. Otherwise we'll throw an exception (see method _check_fields() for more details.) */
        $post = array_merge( array('api_key' => $this->dev_api, 'user_app_key' => $this->user_api), $extra_post_fields );
        /** Merge the APIs with any extra post fields */
        $ch = curl_init();
        /** Start a cURL instance called $ch */
        curl_setopt( $ch, CURLOPT_POST, true );
        /** We will be sending POST fields, tell cURL that we are */
        curl_setopt( $ch, CURLOPT_POSTFIELDS, $post );
        /** Attach the POST fields themselves */
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
        /** Don't output to the browser */
        curl_setopt( $ch, CURLOPT_URL, constant('self::' . strtoupper($method) . '_URL') );
        /** What URL should we post to? */
        $this->reply = curl_exec( $ch );
        /** Get the raw reply, set it to $this->reply for access */
        curl_close( $ch );
        /** Free up the resources */
        $this->_parse_reply( $method );
        /** Parse it, see method below */
        return ( $this->xml->attributes()->status == 'OK' or $this->xml->attributes()->status == '' ) ? true : false;
        /** Sometimes it glitches and a status isn't returned, if this happens, or it equals 'OK', return true. Else return false. */
    }

    /**
     * PingFM_Wrapper::_parse_reply()
     * 
     * @param   string $method  What method are we parsing for.
     * @return  bool   true     Always true upon success
     */
    protected function _parse_reply( $method )
    {
        if ( !$this->reply ) throw new Exception( 'Raw reply could not be found.' );
        $this->xml = $xml = simplexml_load_string( $this->reply );
        /** Parse the raw reply, set it to XML for access from within this function, and to $this->xml if the programmer wants to access it later */
        if ( $xml->attributes()->status == 'FAIL' ) throw new Exception( $this->xml->message );
        $details['transaction'] = ( string )$xml->transaction;
        switch ( $method ):
            case 'validate':
            case 'post':
            case 'tpost':
                /** Nothing to do! - All it returns is the method and the transaction ID. */
                break;
                /** End validate, post, tpost */
            case 'services':
                $details['number_of_services'] = ( int )count( $xml->services->service );
                if ( $details['number_of_services'] > 0 )
                {
                    foreach ( $xml->services->service as $service )
                    {
                        $name = ( string )$service->attributes()->id;
                        $details['services'][$name]['name'] = ( string )$service->attributes()->name;
                        $method = ( string )$service->methods;
                        $methods = ( strstr($method, ',') ) ? explode( ',', $method ) : $method;
                        if ( is_array($methods) )
                        {
                            foreach ( $methods as $method )
                            {
                                $details['services'][$name]['methods'][] = ( string )$method;
                            }
                        }
                        else
                            if ( is_string($methods) )
                            {
                                $details['services'][$name]['methods'][] = ( string )$method;
                            }
                        $details['services'][$name]['number_of_methods'] = count( $details['services'][$name]['methods'] );
                    }
                }
                break;
                /** End services */
            case 'triggers':
                $details['number_of_triggers'] = ( int )count( $xml->triggers->trigger );
                if ( $details['number_of_triggers'] > 0 )
                {
                    foreach ( $xml->triggers->trigger as $trigger )
                    {
                        $name = ( string )$trigger->attributes()->id;
                        $details['triggers'][$name]['name'] = ( string )$trigger->attributes()->id;
                        $details['triggers'][$name]['method'] = ( string )$trigger->attributes()->method;
                        $details['triggers'][$name]['number_of_affected_services'] = count( $trigger->services->service );
                        if ( $details['triggers'][$name]['number_of_affected_services'] > 0 )
                        {
                            foreach ( $trigger->services->service as $service )
                            {
                                $id = ( string )$service->attributes()->id;
                                $details['triggers'][$name]['services'][$id] = ( string )$service->attributes()->name;
                            }
                        }
                    }
                }
                break;
                /** End triggers */
            case 'latest':
                $details['number_of_messages_returned'] = ( int )count( $xml->messages->message );
                if ( $details['number_of_messages_returned'] > 0 )
                {
                    foreach ( $xml->messages->message as $message )
                    {
                        $id = ( string )$message->attributes()->id;
                        $details['messages'][$id]['id'] = ( string )$id;
                        $details['messages'][$id]['method'] = ( string )$message->attributes()->method;
                        $details['messages'][$id]['date']['unix'] = ( string )$message->date->attributes()->unix;
                        $details['messages'][$id]['date']['rfc'] = ( string )$message->date->attributes()->rfc;
                        $details['messages'][$id]['body'] = base64_decode( (string )$message->content->body );
                        $details['messages'][$id]['number_of_services'] = ( int )count( $message->services->service );
                        if ( $details['messages'][$id]['number_of_services'] > 0 )
                        {
                            foreach ( $message->services->service as $service )
                            {
                                $sid = ( string )$service->attributes()->id;
                                $details['messages'][$id]['services'][$sid] = ( string )$service->attributes()->name;
                            }
                        }
                    }
                }
                break;
                /** End latest */
            case 'message':
                $message = $xml->message;
                $details['id'] = ( string )$message->attributes()->id;
                $details['method'] = ( string )$message->attributes()->method;
                $details['date']['unix'] = ( string )$message->date->attributes()->unix;
                $details['date']['rfc'] = ( string )$message->date->attributes()->rfc;
                $details['body'] = base64_decode( (string )$message->content->body );
                $details['number_of_services'] = ( int )count( $message->services->service );
                if ( $details['number_of_services'] > 0 )
                {
                    foreach ( $message->services->service as $service )
                    {
                        $sid = ( string )$service->attributes()->id;
                        $details['services'][$sid] = ( string )$service->attributes()->name;
                    }
                }
                break;
                /** End message */
        endswitch;
        $this->details = $details;
        return true;
    }

    /**
     * PingFM_Wrapper::_check_fields()
     * 
     * @param  string $method  This is used within the class, no need to use this.
     * @param  string $fields  This is used within the class, no need to use this.
     * @return array  $fields  Fixed fields
     */
    protected function _check_fields( $method, $fields )
    {
        if ( in_array($method, array('validate', 'services', 'triggers')) ) return array();
        /** These don't require extra post fields */
        switch ( $method ):
            case 'latest':
                /** Start latest */
                if ( !empty($fields['limit']) ) /** It's optional, only enforce it if it's an non-empty value */
                {
                    $fields['limit'] = intval( $fields['limit'] );
                    /** Has to be an integer */
                    if ( $fields['limit'] < 0 ) /** If it's less than 0 (aka Invalid)... */
                    {
                        $fields['limit'] = 25;
                        /** Set it to the default: 25 */
                    }
                }
                if ( !empty($fields['order']) ) /** As above */
                {
                    $fields['order'] = strtoupper( $fields['order'] );
                    /** It takes ASC or DESC values, make it uppercase just in case */
                    if ( $fields['order'] != 'ASC' and $fields['order'] != 'DESC' ) /** If it's not one of the acceptable values */
                    {
                        $fields['order'] = 'DESC';
                        /** Then set it to the default */
                    }
                }
                break;
                /** End latest */
            case 'message':
                /** Start message */
                if ( empty($fields['message_id']) ) throw new Exception( 'No message_id specified.' );
                /** If (the required field) message_id is not set, throw an exception */
                else /** Otherwise... check if it's good to go */
                {
                    $fields['message_id'] = intval( $fields['message_id'] );
                    /** It like totally has to be an integer */
                    if ( $fields['message_id'] < 0 ) throw new Exception( 'message_id is an invalid negative value.' );
                    /** If it's less than 0, throw an exception */
                }
                break;
                /** End message */
            case 'post':
                /** Start post */
                if ( empty($fields['post_method']) ) throw new Exception( 'post_method is not set.' );
                /** If post_method is not set, or it is not an acceptable value. Throw an error showing acceptable values */
                $fields['post_method'] = strtolower( $fields['post_method'] );
                /** Make it lower */
                if ( !in_array($fields['post_method'], self::$post_methods) ) throw new Exception( 'post_method is not an acceptable value: ' . implode(', ', $this->$post_methods) );
                if ( empty($fields['body']) ) throw new Exception( 'No body set' );
                if ( $fields['post_method'] == 'status' ) /** If it's a status */
                {
                    $fields['body'] = substr( strip_tags($fields['body']), 0, 200 );
                    /** Max 200 chars, tags are stripped - Reflected in above code */
                    if ( isset($fields['title']) ) unset( $fields['title'] );
                    /** It's a status, so we don't need the title, remove it */
                }
                /** End status */
                if ( $fields['post_method'] == 'blog' ) /** If it's a blog */
                {
                    if ( empty($fields['title']) ) throw new Exception( 'A title is required for blogging.' );
                    /** 'blog' option requires a title to be set. */
                    $fields['title'] = strip_tags( $fields['title'] );
                    /** Can't have tags in the title itself */
                }
                /** End blog */
                if ( $fields['post_method'] == 'microblog' )
                {
                    /** If it's a microblog */
                    $fields['body'] = substr( strip_tags($fields['body']), 0, 140 );
                    /** Max 140 chars, tags are stripped - Reflected in above code */
                    if ( isset($fields['title']) ) unset( $fields['title'] );
                    /** It's a microblog, so we don't need the title, remove it */
                }
                /** End microblog */
                if ( is_array($fields['service']) ) /** If it's an array */
                {
                    $fields['service'] = array_map( 'trim', $fields['service'] );
                    /** Remove all excess spaces */
                    $fields['service'] = implode( ',', $fields['service'] );
                    /** And implode with comma */
                } elseif ( is_string($fields['service']) ) /** If it's a string */
                {
                    if ( strlen($fields['service']) < 1 ) /** If it's a zero length string... */
                    {
                        unset( $fields['service'] );
                        /** Then we don't want it - it's optional */
                    }
                }
                if ( $fields['debug'] ) $fields['debug'] = 1;
                /** If debug is equal to true, set it to 1 so we don't post data */
                else  unset( $fields['debug'] );
                /** Otherwise it's optional... unset it */
                break;
                /** End message */
            case 'tpost':
                /** Start tpost */
                if ( empty($fields['trigger']) ) throw new Exception( 'No trigger set.' );
                /** You have to set a trigger, otherwise throw an exception */
                if ( empty($fields['body']) ) throw new Exception( 'No body set.' );
                /** You have to set a body, otherwise throw an exception */
                if ( $fields['debug'] ) $fields['debug'] = 1;
                /** If debug is equal to true, set it to 1 so we don't post data */
                else  unset( $fields['debug'] );
                /** Otherwise it's optional... unset it */
                break;
                /** End tpost */
        endswitch;
        return $fields;
    }
}

/**
 * PingFM_Functions
 * 
 * @package   PingFM_Wrappers
 * @url       http://danltn.com
 * @desc      Set of methods which allow easy access to the Wrapper class
 * @author    Daniel Neville
 * @copyright 2008
 * @version   0.2b
 * @access    public
 * @license   GPL
 */
class PingFM_Functions extends PingFM_Wrapper
{
    /**
     * PingFM_Functions::__construct()
     * 
     * @param  mixed $dev_api
     * @param  mixed $user_api
     * @param  bool  $check
     * @return void
     */
    public function __construct( $dev_api = null, $user_api = null, $check = true )
    {
        parent::__construct( $dev_api, $user_api, $check );
    }

    /**
     * PingFM_Functions::validate()
     * 
     * @return bool $success Whether it ran successfully or not
     */
    public function validate()
    {
        return $this->act( 'validate' );
    }

    /**
     * PingFM_Functions::services()
     * 
     * @return bool $success Whether it ran successfully or not
     */
    public function services()
    {
        return $this->act( 'services' );
    }

    /**
     * PingFM_Functions::triggers()
     * 
     * @return bool $success Whether it ran successfully or not
     */
    public function triggers()
    {
        return $this->act( 'triggers' );
    }

    /**
     * PingFM_Functions::latest()
     * 
     * @param  integer $limit   How many records should we return
     * @param  string  $order   What order (ASC or DESC)
     * @return bool    $success Whether it ran successfully or not
     */
    public function latest( $limit = 25, $order = 'ASC' )
    {
        return $this->act( 'latest', array('limit' => $limit, 'order' => $order) );
    }

    /**
     * PingFM_Functions::message()
     * 
     * @param  integer  $id      What message to return information about
     * @return bool     $success Whether it ran successfully or not
     */
    public function message( $id )
    {
        return $this->act( 'message', array('message_id' => $id) );
    }

    /**
     * PingFM_Functions::post()
     * 
     * @param  string  $post_method  What post method are we using?
     * @param  string  $body         Body of the post request
     * @param  string  $title        Title (optional but required for blog)
     * @param  mixed   $service      Service to affect (optional)
     * @param  integer $debug        Should we actually post, or is this just testing (1 for testing)
     * @return bool    $success      Whether it ran successfully or not
     */
    public function post( $post_method, $body, $title = '', $service = array(''), $debug = 0 )
    {
        return $this->act( 'post', array('body' => $body, 'post_method' => $post_method, 'title' => $title, 'service' => $service, 'debug' => $debug) );
    }

    /**
     * PingFM_Functions::tpost()
     * 
     * @param  string  $trigger What trigger shall will use
     * @param  string  $body    Body of the post request
     * @param  string  $title   Title (optional but required for blog)
     * @param  integer $debug   Should we actually post, or is this just testing (1 for testing)
     * @return bool    $success Whether it ran successfully or not
     */
    public function tpost( $trigger, $body, $title = '', $debug = 0 )
    {
        return $this->act( 'tpost', array('trigger' => $trigger, 'body' => $body, 'title' => $title, 'debug' => $debug) );
    }
}

?>