--- /dev/null
+<?php
+/**
+ * Streamsend API Class
+ *
+ * The Streamsend API class provides simplified interaction with the Streamsend
+ * API. Streamsend is an E-Mail Marketing Solutions provider. Gaslight Media
+ * is considering integrating, or has integrated, Streamsend services into
+ * the Gaslight Media Contact Services applications. This class library intends
+ * to streamline interaction between Gaslight Media applications and the
+ * Streamsend server API. Documentation for the StreamSend API may be found at
+ * the following URL.
+ *
+ * https://app.streamsend.com/docs/api/index.html
+ *
+ * This Class is designed to run under PHP version 5 and require PHP Curl.
+ *
+ * LICENSE: This software constitutes a "trade secret" and "proprietary materials"
+ * of Gaslight Media. It is not available for licensing at this time. Any possession
+ * or use of these materials without the specific written permission of Gaslight
+ * Media is considered a violation of Gaslight Media's rights and will be pursued
+ * to the full extent of available law.
+ *
+ * @category Gaslight Media Proprietary Class Library
+ * @package StreamSend API Integration
+ * @author Gaslight Media <info@gaslightmedia.com>
+ * @copyright 2008 Gaslight Media - All rights Reserved
+ * @license Trade Secret and Proprietary Materials - No License
+ * @version CVS: $Id: class_streamsend_api.inc,v 1.2 2010/03/23 14:28:23 matrix Exp $
+ * @link (none)
+ * @see
+ * @since File available since Release 1.0
+ * @deprecated
+ */
+
+/**
+* Require DocBlock
+*
+* No includes required.
+*
+* PHP libcurl support required.
+*/
+
+/**
+ * ----------------------------------------------------------
+ * Definitions
+ */
+define ("STREAMSEND_MAX_PAGE_SIZE", 10000); // This must be set to the maximum page size for the StreamSend system
+
+/**
+ * API Interface Error Codes
+ *
+ * Also add an error message for each error code
+ */
+ /** Errors related to responses from StreamSend */
+define ("STREAMSEND_API_ERROR_NONE", 0);
+define ("STREAMSEND_API_ERROR_NO_RESPONSE", 1);
+define ("STREAMSEND_API_ERROR_HTTP_RESPONSE", 2);
+define ("STREAMSEND_API_ERROR_BAD_XML", 3);
+
+ /** Errors related to use of the GLM StreamSend methods and functions */
+define ("STREAMSEND_API_ERROR_POSTDATA_NOT_SUPPLIED", 100);
+
+/**
+ * API Interface Error Messages
+ *
+ * Requires an error code for each error message
+ */
+$streamsendErrorMessage = array (
+
+ /** Errors related to responses from StreamSend */
+ STREAMSEND_API_ERROR_NONE => 'No error reported.',
+ STREAMSEND_API_ERROR_NO_RESPONSE => 'No response received from server.',
+ STREAMSEND_API_ERROR_HTTP_RESPONSE => 'Server replied with an unexpected HTTP response code.',
+ STREAMSEND_API_ERROR_BAD_XML => 'The XML response was malformed.',
+
+ /** Errors related to use of the GLM StreamSend methods and functions */
+ STREAMSEND_API_ERROR_POSTDATA_NOT_SUPPLIED => 'No POST data was supplied to send to StreamSend.',
+);
+
+/**
+ * Field Types
+ */
+define("STREAMSEND_API_FIELD_TYPE_TEXT", 1);
+define("STREAMSEND_API_FIELD_TYPE_TEXTAREA", 2);
+define("STREAMSEND_API_FIELD_TYPE_DATETIME", 3);
+define("STREAMSEND_API_FIELD_TYPE_DATE", 4);
+define("STREAMSEND_API_FIELD_TYPE_TIME", 5);
+define("STREAMSEND_API_FIELD_TYPE_SELECT", 6);
+define("STREAMSEND_API_FIELD_TYPE_RADIO", 7);
+define("STREAMSEND_API_FIELD_TYPE_CHECKBOX", 8);
+
+$streamsendFieldTypes = array (
+ STREAMSEND_API_FIELD_TYPE_TEXT => array( 'Name' => 'Text Field', 'Options' => false ),
+ STREAMSEND_API_FIELD_TYPE_TEXTAREA => array( 'Name' => 'Text Area', 'Options' => false ),
+ STREAMSEND_API_FIELD_TYPE_DATETIME => array( 'Name' => 'Date/Time', 'Options' => false ),
+ STREAMSEND_API_FIELD_TYPE_DATE => array( 'Name' => 'Date', 'Options' => false ),
+ STREAMSEND_API_FIELD_TYPE_TIME => array( 'Name' => 'Time', 'Options' => false ),
+ STREAMSEND_API_FIELD_TYPE_SELECT => array( 'Name' => 'Select', 'Options' => true ),
+ STREAMSEND_API_FIELD_TYPE_RADIO => array( 'Name' => 'Radio', 'Options' => true ),
+ STREAMSEND_API_FIELD_TYPE_CHECKBOX => array( 'Name' => 'Checkbox', 'Options' => true )
+);
+
+/**
+ * Other General Setup Data
+ */
+
+$HTTPResponseCodeText = array(
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 300 => 'Multiple Choices',
+ 301 => 'Permanently Moved',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 306 => 'Tempoorary Redirect',
+ 400 => 'Your browser sent a request that this server could not understand. XML included with a request may be malformed',
+ 401 => 'Authorization Required or StreamSend authentication error',
+ 402 => 'Payment Required, plan upgrade required, number of recipients for blast may exceed available quota for account',
+ 403 => 'Forbidden or permission not granted by StreamSend - Contact support staff',
+ 404 => 'Not Found - Possible reasons: The resource never existed, The resource was destroyed, The requested URI is invalid.',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Time-out',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Large',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Requested Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 422 => 'Unprocessable Entity',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency',
+ 425 => 'No code',
+ 426 => 'Upgrade Required',
+ 500 => 'Internal Server Error',
+ 501 => 'Method Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Temporarily Unavailable',
+ 504 => 'Gateway Time-out',
+ 505 => 'HTTP Version Not Supported',
+ 506 => 'Variant Also Negotiates',
+ 507 => 'Insufficient Storage',
+ 510 => 'Not Extended'
+);
+
+
+/**
+ * End of Definitions
+ * ----------------------------------------------------------
+ */
+
+
+
+/**
+ * StreamSend Class
+ */
+
+class StreamSend {
+
+ /**
+ * Field types in StreamSend
+ *
+ * This var is assigned the field type array in the constructor
+ * and is available for reference.
+ */
+ var $streamsendFieldTypes;
+
+ /**
+ * Clase Debug Status
+ *
+ * Set to true to enable debug output
+ */
+ var $debug = false;
+
+ /**
+ * Base URL at which the StreamSend API is located
+ */
+ var $baseURL;
+
+ /**
+ * Complete URL at which the specific StreamSend API method is located
+ */
+ var $completeURL;
+
+ /**
+ * Setting for CURLOPT_SSL_VERIFYPEER
+ *
+ * Defines whether we should verify the peer's certificate
+ *
+ * Normally should be set to "true".
+ */
+ var $verifyPeer;
+
+ /**
+ * Setting for CURLOPT_RETURNTRANSFER
+ *
+ * Defines if PHP curl_exec() should return a string or
+ * display the response directly to the user's browser.
+ *
+ * In all cases this should be "true" to return rather
+ * than display the response.
+ */
+ var $returnTransfer;
+
+ /**
+ * User name to use for authentication into StreamSend API
+ *
+ * The user name and password for the StreamSend API is generated
+ * via the StreamSend partner Web site.
+ */
+ var $userName;
+
+ /**
+ * User password to use for authentication into StreamSend API
+ */
+ var $userPassword;
+
+ /**
+ * XML Request
+ *
+ * This is detail of the request sent to the StreamSend API.
+ */
+ var $xmlRequest = '';
+
+ /**
+ * Default Setting for CURLOPT_HTTPHEADER
+ *
+ * An array of HTTP header fields that is included in all requests.
+ * This is set in the GET, POST, MULTIPART case options in sendRequest().
+ */
+ var $httpHeader = false;
+
+ /**
+ * Setting for CURLOPT_CUSTOMREQUEST
+ *
+ * The HTTP method to use for a request.
+ * For StreamSend API may be: "GET", "POST", "PUT", or "DELETE"
+ */
+ var $requestMethod;
+
+ /**
+ * Curl POST data
+ *
+ * Text to supply to StreamSend in a POST operation
+ * Set to false by default to detect when data is not supplied.
+ */
+ var $postData = false;
+
+ /**
+ * Currently select StreamSend Account
+ *
+ * A StreamSend Account equates to a Gaslight Media customer.
+ * All customer lists and blasts are contained within their account.
+ */
+ var $currentAccount = NULL;
+
+ /**
+ * Int - Last Curl Return Code
+ */
+ var $curlError = NULL;
+
+ /**
+ * String - Last Curl Return String
+ */
+ var $curlErrorString = NULL;
+
+ /**
+ * Boolean - Was last API request successful
+ */
+ var $responseStatus = NULL;
+
+ /**
+ * Numeric Error Code we assign to API failures
+ */
+ var $responseError = NULL;
+
+ /**
+ * Text error message describing $responseError
+ */
+ var $responseErrorText = NULL;
+
+ /**
+ * API Server HTTP Response Code
+ *
+ * The response HTTP response code from the last request to the StreamSend server.
+ * (i.e. 200, 404, ...)
+ */
+ var $responseHTTPStatus = NULL;
+
+ /**
+ * Text describing HTTP Response code
+ */
+ var $responseHTTPStatusText = NULL;
+
+ /**
+ * API XML Response
+ *
+ * The raw XML response from the last API request to the StreamSend server.
+ * $noXMLExpected tells the sendRequest() method to no bother trying to
+ * parse the response from StreamSend as XML.
+ */
+ var $response = NULL;
+ var $noXMLExpected = false;
+
+ /**
+ * Return headers in response from curl.
+ *
+ * If this is true, curl will include the response headers in the response.
+ */
+ var $returnHeaders = false;
+
+ /**
+ * Destination for parsed header data returned in a response when $returnHeaders is true
+ */
+ var $parsedHeaders = false;
+
+ /**
+ * Parsed API Response Data
+ *
+ * This is an array containing the parsed data from the XML response.
+ */
+ var $responseData = NULL;
+
+ /**
+ * Display a debug message if debug is enabled
+ */
+ var $debugBuffer = '';
+
+ function debug($message)
+ {
+ if ($this->debug)
+ $this->debugBuffer .= '<p><b>StreamSend API Debug: </b>'.$message.'<br>';
+ }
+
+ /**
+ * Parse headers into a key/value array
+ *
+ */
+ function parseHeaders( $header )
+ {
+ $retVal = array();
+
+ $fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $header));
+
+ foreach( $fields as $field ) {
+
+ if( preg_match('/([^:]+): (.+)/m', $field, $match) ) {
+
+ $match[1] = preg_replace('/(?<=^|[\x09\x20\x2D])./e', 'strtoupper("\0")', strtolower(trim($match[1])));
+
+ if( isset($retVal[$match[1]]) ) {
+ $retVal[$match[1]] = array($retVal[$match[1]], $match[2]);
+ } else {
+ $retVal[$match[1]] = trim($match[2]);
+ }
+ }
+ }
+ return $retVal;
+ }
+
+ /** StreamSend
+ * Class construtor. Set up the default variables for the class.
+ *
+ * May be called with optional $debug parameter to set debug right away.
+ */
+ function StreamSend($base_url, $user_name, $user_password, $debug = false )
+ {
+ global $streamsendFieldTypes;
+
+ $this->baseURL = $base_url;
+ $this->verifyPeer = true;
+ $this->returnTransfer = true;
+ $this->userName = $user_name;
+ $this->userPassword = $user_password;
+ $this->debug = $debug;
+ $this->streamsendFieldTypes = $streamsendFieldTypes;
+
+ $this->debug("StreamSend() Constructor called with: <br>
+ URL: $base_url<br>
+ User Name: $user_name<br>
+ User Password: $user_password<br>");
+ }
+
+ /**
+ * Sets and API error code along with the corresponding error description.
+ *
+ * If the error code is anything but 0 (successful), it must be a failure
+ */
+ function setAPIError($error)
+ {
+ global $streamsendErrorMessage;
+
+ $this->responseError = $error;
+ $this->responseErrorText = $streamsendErrorMessage[$error];
+ $this->responseStatus = ( $error > 0 );
+ }
+
+ function sendRequest($method, $uri )
+ {
+ global $HTTPResponseCodeText;
+
+ $this->debug("sendRequest() called with: <br>$uri");
+
+ /** Build complete URL to specific StreamSend API method */
+ $this->completeURL = $this->baseURL.'/'.$uri;
+
+ /** Create curl object */
+ $c = curl_init( );
+
+ /** Setup Curl Parameters */
+ curl_setopt($c, CURLOPT_URL, $this->completeURL);
+ curl_setopt($c, CURLOPT_SSL_VERIFYPEER, $this->verifyPeer);
+ curl_setopt($c, CURLOPT_RETURNTRANSFER, $this->returnTransfer);
+ curl_setopt($c, CURLOPT_USERPWD, $this->userName.':'.$this->userPassword);
+ curl_setopt($c, CURLOPT_POST, false);
+ curl_setopt($c, CURLOPT_POSTFIELDS, NULL);
+
+ /** set request method */
+ switch ($method) {
+ case 'PUT' :
+ $this->httpHeader = array (
+ 'Accept: application/xml', # any data returned should be XML
+ 'Content-Type: application/xml' # any data we send will be XML
+ );
+ curl_setopt($c, CURLOPT_HTTPHEADER, $this->httpHeader);
+ curl_setopt($c, CURLOPT_CUSTOMREQUEST, 'PUT');
+ curl_setopt($c, CURLOPT_POST, true);
+
+ /** Check for post data */
+ if( !$this->postData ) {
+ $this->setAPIError(STREAMSEND_API_ERROR_POSTDATA_NOT_SUPPLIED);
+ return(false);
+ }
+
+ /** Provide the post data */
+ curl_setopt($c, CURLOPT_POSTFIELDS, $this->postData);
+ break;
+ case 'GET':
+ $this->httpHeader = array (
+ 'Accept: application/xml', # any data returned should be XML
+ 'Content-Type: application/xml' # any data we send will be XML
+ );
+ curl_setopt($c, CURLOPT_HTTPHEADER, $this->httpHeader);
+ curl_setopt($c, CURLOPT_HTTPGET, true);
+
+ break;
+
+ case 'DELETE':
+ $this->httpHeader = array (
+ 'Accept: application/xml' # any data returned should be XML
+ );
+ curl_setopt($c, CURLOPT_HTTPHEADER, $this->httpHeader);
+ curl_setopt($c, CURLOPT_CUSTOMREQUEST, 'DELETE');
+
+ break;
+
+ case 'POST':
+
+ $this->httpHeader = array (
+ 'Accept: application/xml', # any data returned should be XML
+ 'Content-Type: application/xml' # any data we send will be XML
+ );
+ curl_setopt($c, CURLOPT_HTTPHEADER, $this->httpHeader);
+ curl_setopt($c, CURLOPT_POST, true);
+
+ /** Check for post data */
+ if( !$this->postData ) {
+ $this->setAPIError(STREAMSEND_API_ERROR_POSTDATA_NOT_SUPPLIED);
+ return(false);
+ }
+
+ /** Provide the post data */
+ curl_setopt($c, CURLOPT_POSTFIELDS, $this->postData);
+ break;
+
+ case 'MULTIPART':
+ /** This method sends multipart/form-data for uploading files */
+
+ $this->httpHeader = array (
+ 'Accept: application/xml', # any data returned should be XML
+ );
+ curl_setopt($c, CURLOPT_HTTPHEADER, $this->httpHeader);
+ curl_setopt($c, CURLOPT_POST, true);
+
+ /** Check for post data */
+ if( !$this->postData ) {
+ $this->setAPIError(STREAMSEND_API_ERROR_POSTDATA_NOT_SUPPLIED);
+ return(false);
+ }
+
+ /** Provide the post data */
+ curl_setopt($c, CURLOPT_POSTFIELDS, $this->postData);
+ break;
+
+ default:
+ curl_setopt($c, CURLOPT_CUSTOMREQUEST, $method);
+ break;
+ }
+
+ /** Clear all response data */
+ $this->setAPIError(STREAMSEND_API_ERROR_NONE);
+ $this->responseHTTPStatus = NULL;
+ $this->response = NULL;
+ $this->responseData = NULL;
+
+ /** If requested, include the returned headers in the respone output */
+ if( $this->returnHeaders )
+ curl_setopt($c, CURLOPT_HEADER, true);
+
+ /**
+ * Send the request to the StreamSend server.
+ *
+ * If there's a complete failure to talk with it, return false.
+ */
+ if (!($this->response = curl_exec($c))) {
+ $this->setAPIError(STREAMSEND_API_ERROR_NO_RESPONSE);
+ $this->curlError = curl_errno($c);
+ $this->curlErrorString = curl_error($c);
+ $this->responseErrorText = curl_error($c);
+ $this->debug('sendRequest() CURL Error: <br>'.$this->responseErrorText);
+ return(clone $this);
+ }
+
+ $this->debug('sendRequest() Call to curl_exec() completed<br>Full Response<br /><table border="1"><tr><td><pre>'
+ ."\n".htmlentities($this->response)."\n"
+ .'</pre></td></tr></table>');
+
+
+ /**
+ * We received some kind of response from the StreamSend server.
+ *
+ * Save the returned HTTP result status (i.e. 200, 404, etc)
+ */
+ $this->responseHTTPStatus = curl_getinfo($c, CURLINFO_HTTP_CODE);
+ $this->debug('sendRequest() HTTP response code returned: '.$this->responseHTTPStatus);
+
+ /**
+ * Check for if the HTTP result status indicates a problem
+ */
+ $this->responseHTTPStatusText = $HTTPResponseCodeText[$this->responseHTTPStatus];
+ if ($this->responseHTTPStatus > 299) {
+ $this->debug('sendRequest() Received unexpected HTTP response status ('.$this->responseHTTPStatus.').<br>'.$this->responseHTTPStatusText);
+ $this->setAPIError(STREAMSEND_API_ERROR_HTTP_RESPONSE);
+ }
+
+ /**
+ * If we have a failed HTTP reponse then return now
+ */
+ if ($this->responseError > 0) {
+ return( $this->responseError );
+ }
+
+ /**
+ * If there's no data returned, assume that's what was expected
+ * and return.
+ */
+ $response = trim($this->response);
+ if (empty($response)) {
+ $this->debug('sendRequest() No response provided. Assuming it was not expected. Returning.');
+ $this->setAPIError(STREAMSEND_API_ERROR_NONE);
+ return( $this->responseError );
+ }
+
+ /**
+ * If return headers were requested, parse them out into an array
+ */
+ if( $this->returnHeaders )
+ $this->parsedHeaders = $this->parseHeaders($response);
+
+ /** If no XML is expected, return now */
+ if( $this->noXMLExpected )
+ return $this->responseError;
+
+ /**
+ * Parse the XML response to a data array.
+ */
+ $this->debug('sendRequest() Have text of a response. Trying to parse XML.');
+ try {
+ $this->responseData = new SimpleXMLElement($response);
+ } catch (Exception $e) {
+ $this->setAPIError(STREAMSEND_API_ERROR_BAD_XML);
+ return( $this->responseError );
+ }
+ $this->setAPIError(STREAMSEND_API_ERROR_NONE);
+ $this->debug('sendRequest() Result of parsed XML<br /><table border="1"><tr><td><pre>'.htmlentities(print_r($this->responseData,true)).'</pre></td></tr></table><p>');
+
+ return( $this->responseError );
+ }
+
+ /*****************************************************************************************************
+ *
+ * STREAMSEND RESOURCE CATEGORIES
+ *
+ * The StreamSend API is divided into general categories of functionality. Below are groupings of
+ * methods intended to address GLM needs to interact with the StreamSend API under each category.
+ *
+ * System Configuration
+ * Audiences Top level grouping - There is an Audience ID for each account that doesn't change
+ * Fields Attributes that describe each contact who receives E-Mail from a blast
+ * Fields Options Available options for contact picklist fields
+ * Accounts Customer level functionality
+ * Users Users who have access to specific accounts
+ *
+ * Contacts and Contact Groupings
+ * People Individual contacts
+ * Lists Groups of contacts
+ * Memberships Links contacts to lists (subscriptions)
+ * Filters Means of grouping lists and filtering contacts for use in a blast
+ *
+ * E-Mail Blasts
+ * Emails E-Mails (newsletters) that may be sent
+ * Blasts An individual instance of E-Mail distribution
+ * Bounces Bounce reporting for a specific Blast
+ * Unsubscribes Unsubscribe requests resulting from a specific blast
+ * Clicks Instances of contacts clicking on a link in an E-Mail
+ * Views Instances of a contact viewing an E-Mail
+ *
+ *****************************************************************************************************/
+
+
+ /** ---------------------------------- System Configuration Methods -------------------------------- */
+
+
+ /**
+ * Audience Methods
+ *
+ * Audiences form the highest level grouping of people. Within each audience, people may be grouped
+ * into any number of lists, but audiences themselves are self-contained and mutually exclusive.
+ * Each StreamSend account has one audience, as StreamSend does not currently provide support for
+ * multiple audiences per account. This resource is provided for conceptual consistency and is
+ * limited to the index, show, and update actions.
+ *
+ * Each account had an Audience ID that doesn't change, but it's not the same for all accounts. This
+ * method can be used to determine the correct ID for an account.
+ * another request to the StreamSend API.
+ */
+
+ function audienceList()
+ {
+ $this->debug('audienceList() called');
+
+ /** This operation requires the "GET" Method */
+ if ($this->sendRequest('GET', 'audiences') === false) {
+ $this->debug('audienceList() Call to sendRequest() failed.');
+ return( false );
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('audienceList() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('audienceList() Received good response from sendRequest().');
+ }
+
+ return(clone $this);
+ }
+
+ /**
+ * Fields Methods
+ *
+ * Fields are the various attributes that describe each person, e.g. first name, last name.
+ * Each account may have a unique set of fields. Be sure to maintain a standard set of
+ * fields to ensure compatibility with Gaslight Media applications.
+ */
+
+ function fieldCreate( $name, $type, $options = false, $include_blank = false, $allow_other = false )
+ {
+ global $streamsendFieldTypes;
+
+ $this->debug('fieldCreate() called to create field type: '.$streamsendFieldTypes[$type]['Name']);
+
+ /** Make sure a valid name is supplied */
+ if( ($name = trim($name)) == '') {
+ $this->debug('fieldCreate() Name not provided or invalid.');
+ return false;
+ }
+
+ /** Force type to be numerit and make sure the specified field type is valid. */
+ $type = ($type + 0);
+ if( $type == 0 || !isset($streamsendFieldTypes[$type]) ) {
+ $this->debug('fieldCreate() Field type not provided or not a valid type number.');
+ return false;
+ }
+
+ /** If the field type requires options then make sure at least one was provided */
+ if( $streamsendFieldTypes[$type]['Options'] && ( !is_array($options) || count($options) == 0 ) ) {
+ $this->debug('fieldCreate() Options not provided for field type that requires options.');
+ return false;
+ }
+
+ /**
+ * Build XML POST data string for supplied field
+ */
+ $post_data = "<field>\n";
+ $post_data .= " <name>$name</name>\n"
+ ." <data-type>".$streamsendFieldTypes[$type]['Name']."</data-type>\n"
+ ." <include_blank>".($include_blank?'true':'false')."</include_blank>\n"
+ ." <allow_other>".($allow_other?'true':'false')."</allow_other>\n";
+ if( $streamsendFieldTypes[$type]['Options'] ) {
+ $post_data .= " <options>\n";
+ foreach( $options as $opt )
+ $post_data .= " <option>$opt</option>\n";
+ $post_data .= " </options>\n";
+ }
+ $post_data .= "</field>\n";
+
+ $this->postData = $post_data;
+ $this->debug('fieldCreate() XML POST request data<br /><table border="1"><tr><td><pre>'
+ ."\n".htmlentities($this->postData)."\n"
+ .'</pre></td></tr></table>');
+
+ /** Tell sendRequest() to not bother parsing response as XML and to return response headers for parsing */
+ $this->noXMLExpected = true;
+ $this->returnHeaders = true;
+
+ /** This operation requires the "POST" Method */
+ if ($this->sendRequest('POST', 'audiences/'.STREAMSEND_AUDIENCE.'/fields') === false) {
+ $this->debug('fieldCreate() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /** Be sure to reset these parameters or other functions may have trouble */
+ $this->noXMLExpected = false;
+ $this->returnHeaders = false;
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('fieldCreate() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('fieldCreate() Received good response from sendRequest().');
+ }
+
+ /** Did we get a 422 response? If so, it could be that the field already exists */
+ if ($this->responseHTTPStatus != '201' ) {
+ $this->debug('fieldCreate() StreamSend returned a "422 Unprocessable Entry" response. Does the field already exist?');
+ return false;
+ }
+
+ /** We should have received a 201 response */
+ if ($this->responseHTTPStatus != '201' ) {
+ $this->debug('fieldCreate() StreamSend did not return a "201 Created" response.');
+ return false;
+ }
+
+ /** Check for the field ID in the "Location:" header line */
+ if( !isset($this->parsedHeaders['Location']) ) {
+ $this->debug('fieldCreate() StreamSend did not return a "Location:" header with the field ID.');
+ return false;
+ }
+
+ /** Get field ID from "Location" header and force it to a number, then check it's not 0 */
+ if( ($field_id = substr( strrchr( $this->parsedHeaders['Location'], '/' ), 1 ) + 0) == 0 ) {
+ $this->debug('fieldCreate() StreamSend did not return a valid field ID. ('.$id.')');
+ return false;
+ }
+
+ $this->debug('fieldCreate() Have a valid field ID. ('.$field_id.')');
+
+ $this->responseData->fieldID = $field_id;
+
+ return clone $this;
+
+ }
+
+
+ function fieldsList()
+ {
+ $this->debug('fieldsList() called');
+
+ /** This operation requires the "GET" Method */
+ if ($this->sendRequest('GET', 'audiences/'.STREAMSEND_AUDIENCE.'/fields') === false) {
+ $this->debug('fieldsList() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('fieldsList() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('fieldsList() Received good response from sendRequest().');
+ }
+
+ return(clone $this);
+ }
+
+ /**
+ * Field Get
+ *
+ * Fields are the various attributes that describe each person, e.g. first name, last name.
+ * Each account may have a unique set of fields. Be sure to maintain a standard set of
+ * fields to ensure compatibility with Gaslight Media applications.
+ */
+
+ function fieldGet( $id )
+ {
+ $this->debug('fieldGet() called');
+
+ /** This operation requires the "GET" Method */
+ if ($this->sendRequest('GET', 'audiences/'.STREAMSEND_AUDIENCE.'/fields/'.$id) === false) {
+ $this->debug('fieldGet() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('fieldGet() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('fieldGet() Received good response from sendRequest().');
+ }
+
+ return(clone $this);
+ }
+
+ function fieldOptionCreate($fieldId, $name)
+ {
+ global $streamsendFieldTypes;
+
+ $this->debug('fieldOptionCreate() called to create field type: '.$streamsendFieldTypes[$type]['Name']);
+
+ /** Make sure a valid name is supplied */
+ if( ($name = trim($name)) == '') {
+ $this->debug('fieldOptionCreate() Name not provided or invalid.');
+ return false;
+ }
+
+ /** Force fieldId to be numeric and make sure the specified field type is valid. */
+ $fieldId = ($fieldId + 0);
+ if( $fieldId == 0 || !is_numeric($fieldId) ) {
+ $this->debug('fieldOptionCreate() FieldId not provided or not a number.');
+ return false;
+ }
+
+ /**
+ * Build XML POST data string for supplied field
+ */
+ $post_data = "<option>\n";
+ $post_data .= " <name>$name</name>\n";
+ $post_data .= "</option>\n";
+
+ $this->postData = $post_data;
+ $this->debug('fieldOptionCreate() XML POST request data<br /><table border="1"><tr><td><pre>'
+ ."\n".htmlentities($this->postData)."\n"
+ .'</pre></td></tr></table>');
+
+ /** Tell sendRequest() to not bother parsing response as XML and to return response headers for parsing */
+ $this->noXMLExpected = true;
+ $this->returnHeaders = true;
+
+ /** This operation requires the "POST" Method */
+ $postUrl = "audiences/".STREAMSEND_AUDIENCE."/fields/{$fieldId}/options";
+ if ($this->sendRequest('POST', $postUrl) === false) {
+ $this->debug('fieldOptionCreate() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /** Be sure to reset these parameters or other functions may have trouble */
+ $this->noXMLExpected = false;
+ $this->returnHeaders = false;
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('fieldOptionCreate() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('fieldOptionCreate() Received good response from sendRequest().');
+ }
+
+ /** Did we get a 404 response? */
+ if ($this->responseHTTPStatus == '404' ) {
+ $this->debug('fieldOptionCreate() StreamSend returned a "404 Page Not Found" response.');
+ return false;
+ }
+
+ /** Did we get a 422 response? If so, it could be that the field already exists */
+ if ($this->responseHTTPStatus == '422' ) {
+ $this->debug('fieldOptionCreate() StreamSend returned a "422 Unprocessable Entry" response. Does the field already exist?');
+ return false;
+ }
+
+ /** We should have received a 201 response */
+ if ($this->responseHTTPStatus != '201' ) {
+ $this->debug('fieldOptionCreate() StreamSend did not return a "201 Created" response.');
+ return false;
+ }
+
+ /** Check for the field ID in the "Location:" header line */
+ if( !isset($this->parsedHeaders['Location']) ) {
+ $this->debug('fieldOptionCreate() StreamSend did not return a "Location:" header with the field ID.');
+ return false;
+ }
+
+ /** Get field ID from "Location" header and force it to a number, then check it's not 0 */
+ if( ($optionId = substr( strrchr( $this->parsedHeaders['Location'], '/' ), 1 ) + 0) == 0 ) {
+ $this->debug('fieldOptionCreate() StreamSend did not return a valid option ID. ('.$id.')');
+ return false;
+ }
+
+ $this->debug('fieldOptionCreate() Have a valid field ID. ('.$optionId.')');
+
+ $this->responseData->optionID = $optionId;
+
+ return clone $this;
+ }
+
+ function fieldOptionDelete($fieldId, $optionId)
+ {
+ $this->debug('fieldOptionDelete() Called to delete Field Option '.$optionId );
+
+ /** Force fieldId to be numeric and make sure the specified field type is valid. */
+ $fieldId = ($fieldId + 0);
+ if( $fieldId == 0 || !is_numeric($fieldId) ) {
+ $this->debug('fieldOptionDelete() FieldId not provided or not a number.');
+ return false;
+ }
+
+ /** Force optionId to be numeric and make sure the specified field type is valid. */
+ $optionId = ($optionId + 0);
+ if( $optionId == 0 || !is_numeric($optionId) ) {
+ $this->debug('fieldOptionDelete() OptionId not provided or not a number.');
+ return false;
+ }
+
+ /** Expecting only an HTTP response code, so no XML parsing is required */
+ $this->noXMLExpected = true;
+
+ /** This operation requires the "DELETE" Method */
+ $deleteUrl = "audiences/"
+ . STREAMSEND_AUDIENCE
+ . "/fields/{$fieldId}/options/{$optionId}/";
+ if ($this->sendRequest('DELETE', $deleteUrl) === false) {
+ $this->debug('fieldOptionDelete() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /** Be sure to reset these parameters or other functions may have trouble */
+ $this->noXMLExpected = false;
+
+ /** Process the two normally expected response codes related to deleting a contact */
+ switch( $this->responseHTTPStatus ) {
+
+ case '200':
+ $this->debug('fieldOptionDelete() Call to sendRequest() returned a 200 OK. Contact should be deleted.');
+ return clone $this;
+ break;
+
+ case '423':
+ $this->debug('fieldOptionDelete() Call to sendRequest() returned a 423 LOCKED. Contact cannot be deleted at this time.');
+ return clone $this;
+ break;
+
+ }
+
+ /** We get here because an unexpected HTTP response code was recieved */
+ $this->debug('fieldOptionDelete() Call to sendRequest() returned an unexpected code. Contact may not be deleted.');
+ return false;
+ }
+
+ /**
+ * Fields Options Methods
+ *
+ * Those fields that utilize enumerated values, e.g. Select, Radio, Checkbox, will present
+ * each person with a series of options from which they may choose. The available options
+ * for a given field constitute an ordered set onto which new options will be appended.
+ */
+
+ function fieldOptionsGet( $id )
+ {
+ $this->debug('fieldOptionsGet() called');
+
+ /** This operation requires the "GET" Method */
+ if ($this->sendRequest('GET', 'audiences/'.STREAMSEND_AUDIENCE.'/fields/'.$id.'/options') === false) {
+ $this->debug('fieldOptionsGet() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('fieldOptionsGet() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('fieldOptionsGet() Received good response from sendRequest().');
+ }
+
+ return(clone $this);
+ }
+
+ /**
+ * To update a given option for a field.
+ *
+ * @global type $streamsendFieldTypes (why we still using globals?)
+ *
+ * @param type $fieldId
+ * @param type $optionId
+ * @param type $name
+ *
+ * @return type
+ */
+ function fieldOptionUpdate($fieldId, $optionId, $name)
+ {
+ global $streamsendFieldTypes;
+
+ $this->debug('fieldOptionUpdate() called to create field type: '.$streamsendFieldTypes[$type]['Name']);
+
+ /** Make sure a valid name is supplied */
+ if( ($name = trim($name)) == '') {
+ $this->debug('fieldOptionUpdate() Name not provided or invalid.');
+ return false;
+ }
+
+ /** Force fieldId to be numeric and make sure the specified field type is valid. */
+ $fieldId = ($fieldId + 0);
+ if( $fieldId == 0 || !is_numeric($fieldId) ) {
+ $this->debug('fieldOptionUpdate() FieldId not provided or not a number.');
+ return false;
+ }
+
+ /** Force optionId to be numeric and make sure the specified field type is valid. */
+ $optionId = ($optionId + 0);
+ if( $optionId == 0 || !is_numeric($optionId) ) {
+ $this->debug('fieldOptionUpdate() OptionId not provided or not a number.');
+ return false;
+ }
+
+ /**
+ * Build XML POST data string for supplied field
+ */
+ $post_data = "<option>\n";
+ $post_data .= " <name>$name</name>\n";
+ $post_data .= "</option>\n";
+
+ $this->postData = $post_data;
+ $this->debug('fieldOptionUpdate() XML POST request data<br /><table border="1"><tr><td><pre>'
+ ."\n".htmlentities($this->postData)."\n"
+ .'</pre></td></tr></table>');
+
+ /** This operation requires the "POST" Method */
+ $putUrl = "audiences/".STREAMSEND_AUDIENCE."/fields/{$fieldId}/options/{$optionId}/";
+ if ($this->sendRequest('PUT', $putUrl) === false) {
+ $this->debug('fieldOptionUpdate() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('fieldOptionUpdate() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('fieldOptionUpdate() Received good response from sendRequest().');
+ }
+
+ return clone $this;
+ }
+
+ function fieldsGetAll()
+ {
+ $this->debug('fieldsGetAll() called');
+
+ if (!($fields = $this->fieldsList($field_id)) || $fields->responseStatus) {
+ $this->debug('fieldsGetAll() Call to fieldGet() failed.');
+ return false;
+ }
+
+ $this->fieldsData = array();
+
+ foreach ($fields->responseData->field as $field) {
+
+ $f_id = (int) $field->id;
+
+ /** Add the field data to the plain fieldsData array */
+ $this->fieldsData[$f_id] = array(
+ 'name' => ( (string) $field->name ),
+ 'data-type' => ( (string) $field->{'data-type'} ),
+ 'include-blank' => ( (string) $field->{'include-blank'} ),
+ 'allow-other' => ( (string) $field->{'allow-other'} ),
+ 'slug' => ( (string) $field->slug )
+ );
+
+
+ switch( $field->{'data-type'} ) {
+
+ /** These fields have no additional data */
+ case 'Text Field':
+ case 'Text Area':
+ case 'Date/Time':
+ case 'Date':
+ case 'Time':
+ $this->fieldsData[$f_id]['options'] = false;
+ break;
+
+ /** These fields have options associated with them */
+ case 'Select':
+ case 'Radio':
+ case 'Checkbox':
+
+ /** Get the options for this field - Don't fail completely here */
+ if (!($options = $this->fieldOptionsGet($f_id)) || $options->responseStatus) {
+ $this->debug('fieldsGetAll() Call to fieldOptionsGet() failed.');
+ $fields_data[$f_id]['Options'] = false;
+ break;
+ }
+
+
+ //$fields_data
+ foreach( $options->responseData->option as $option ) {
+ $this->fieldsData[$f_id]['options'][(int) $option->id] = (string) $option->name;
+ }
+
+ break;
+ }
+
+ }
+
+ return(clone $this);
+ }
+
+ /**
+ * Accounts Methods
+ *
+ * Resellers of StreamSend may create additional accounts underneath their parent account.
+ * The quota for child accounts constitutes a subset of the quota for the parent reseller account.
+ *
+ * Gaslight Media will have one account per customer or Web site. In some cases, customers may
+ * want to conduct E-Mail blasts for multiple related Web sites.
+ */
+ function accountList()
+ {
+ $this->debug('accountList() called');
+
+ /**
+ * This operation requires the "GET" Method
+ * A false indicates that the server didn't respond at all.
+ */
+ if ($this->sendRequest('GET', 'accounts') === false) {
+ $this->debug('accountList() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('accountList() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('accountList() Received good response from sendRequest().');
+ }
+
+ return(clone $this);
+ }
+
+ function accountGet( $id )
+ {
+ $this->debug('acccountGet() called');
+
+ /** This operation requires the "GET" Method */
+ if ($this->sendRequest('GET', 'accounts/'.$id) === false) {
+ $this->debug('accountGet() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('accountGet() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('accoun tGet() Received good response from sendRequest().');
+ }
+
+ /** Stuff the results into a logical and consistent place to use by the calling program */
+ $this->contact = $this->responseData;
+
+ return clone $this;
+ }
+
+
+
+ /**
+ * Destroy Account - USE WITH CAUTION!!!
+ * Seems to take the StreamSend servers some time to complete and may
+ * cause their systems to be unresponsive or require killing the cookie for
+ * the Web login to their account management interface and loging in again.
+ *
+ * This may have to be used to remove an account since it's not possible as far
+ * as I can tell to change the name on a sub-account and no way to delete it from
+ * the Web interface. This does totally remove it.
+ *
+ * @return unknown_type
+ */
+ function accountDestroy( $id )
+ {
+ $this->debug('accountDestroy() called');
+
+ /**
+ * This operation requires the "GET" Method
+ * A false indicates that the server didn't respond at all.
+ */
+ if ($this->sendRequest('DELETE', 'accounts/'.$id) === false) {
+ $this->debug('accountDestroy() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('accountDestroy() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('accountDestroy() Received good response from sendRequest().');
+ }
+
+ return(clone $this);
+ }
+
+
+
+
+
+ /**
+ * User Methods
+ *
+ * One may manage the users that have access to one�s account or, if one is a
+ * reseller, the users that have access to one�s resold accounts.
+ *
+ * Gaslight Media will normally have at least one user per account to permit
+ * the customer access to the StreamSend interface.
+ */
+
+ function userList()
+ {
+ $this->debug('userList() called');
+
+ /** This operation requires the "GET" Method */
+ if ($this->sendRequest('GET', 'users') === false) {
+ $this->debug('userList() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('userList() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('userList() Received good response from sendRequest().');
+ }
+
+ return(clone $this);
+ }
+
+
+ /** ------------------------------ Contacts and Contact Grouping Methods --------------------------- */
+
+ /**
+ * People (contacts) Methods
+ *
+ * People are the recipients of mailings.
+ */
+
+ function contactList( $page = 1, $pagesize = 100 )
+ {
+ $this->debug('contactList() called');
+
+ /** Check for and limit to the maximum page size **/
+ if ($pagesize > STREAMSEND_MAX_PAGE_SIZE) {
+ $pagesize = STREAMSEND_MAX_PAGE_SIZE;
+ $this->debug('contactList() Maximum page size exceeded. Page size set to StreamSend maximum.');
+ }
+
+ /** This operation requires the "GET" Method */
+ if ($this->sendRequest('GET', 'audiences/'.STREAMSEND_AUDIENCE.'/people?page='.$page.'&per_page='.$pagesize) === false) {
+ $this->debug('contactList() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('contactList() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('contactList() Received good response from sendRequest().');
+ }
+
+ /** Place list of "persons" in results as "contacts" */
+ $this->contacts = $contacts->responseData->person;
+
+ return clone $this;
+ }
+
+ function contactSearch( $email )
+ {
+ $this->debug('contactSearch() called');
+
+ /** This operation requires the "GET" Method */
+ if ($this->sendRequest('GET', 'audiences/'.STREAMSEND_AUDIENCE.'/people?email_address='.$email) === false) {
+ $this->debug('contactSearch() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('contactSearch() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('contactSearch() Received good response from sendRequest().');
+ }
+
+ /**
+ * We now need to see if we actually got any contact data back
+ */
+ $this->contact = false;
+ foreach ($this->responseData->person as $contact) {
+
+ /** Check to see if there's more than one contact returned, which would be an error */
+ if( $this->contact != false ) {
+ $this->debug('contactSearch() Received mulitple contact results. This should not happen.');
+ return false;
+ }
+
+ /** Stuff the results into a logical and consistent place to use by the calling program */
+ $this->contact = $contact;
+ }
+
+ return clone $this;
+ }
+
+ function contactGet( $id )
+ {
+ $this->debug('contactGet() called');
+
+ /** This operation requires the "GET" Method */
+ if ($this->sendRequest('GET', 'audiences/'.STREAMSEND_AUDIENCE.'/people/'.$id) === false) {
+ $this->debug('contactGet() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('contactGet() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('contactGet() Received good response from sendRequest().');
+ }
+
+ /** Stuff the results into a logical and consistent place to use by the calling program */
+ $this->contact = $this->responseData;
+
+ return(clone $this);
+ }
+
+ /**
+ * Create a Contact
+ *
+ * $contact is field/value array.
+ *
+ * The index of contactData is the field name slug (i.e. field name = "First Name", slug = "first_name" )
+ * Slugs may only contain lower-case alphanumeric characters and "_".
+ *
+ * Minimum required fields include: email_address
+ *
+ * See setup file for explanation on other default values.
+ */
+
+ function contactCreate( $contact,
+ $activate = STREAMSEND_DEFAULT_ACTIVATE,
+ $deliver = STREAMSEND_DEFAULT_DELIVER_ACTIVATION,
+ $welcome = STREAMSEND_DEFAULT_DELIVER_WELCOME )
+ {
+ $this->debug('contactCreate() called');
+
+ /**
+ * Check for minimum contact data
+ */
+ if( !isset($contact['email-address']) || preg_match( "/^[A-Z0-9._%-]+@[A-Z0-9._%-]+\.[A-Z]{2,4}$/i", $contact['email-address'] ) == 0 ) {
+ $this->debug('contactCreate() E-Mail address not provided or not a valid E-Mail address.');
+ return false;
+ }
+
+ /**
+ * Build XML POST data string for supplied contact
+ */
+ $post_data = "<person>\n";
+ while( list($k, $v) = each($contact) ) {
+ $v = str_replace("&", "&", $v);
+ $v = str_replace("'", "'", $v);
+ if (strstr($v, "|")) {
+ $mSect = explode("|", $v);
+ foreach ($mSect as $multiples) {
+ $post_data .= ' <'.trim($k).'>'.trim($multiples).'</'.trim($k).">\n";
+ }
+ } else {
+ $post_data .= ' <'.trim($k).'>'.trim($v).'</'.trim($k).">\n";
+ }
+ }
+ $post_data .= " <activate>$activate</activate>\n"
+ ." <deliver-activation>$deliver</deliver-activation>\n"
+ ." <deliver-welcome>$welcome</deliver-welcome>\n";
+ $post_data .= "</person>\n";
+ $this->postData = $post_data;
+ $this->debug('contactCreate() XML POST request data<br /><table border="1"><tr><td><pre>'
+ ."\n".htmlentities($this->postData)."\n"
+ .'</pre></td></tr></table>');
+
+ /** This operation requires the "POST" Method */
+ if ($this->sendRequest('POST', 'audiences/'.STREAMSEND_AUDIENCE.'/people') === false) {
+ $this->debug('contactCreate() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('contactCreate() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('contactCreate() Received good response from sendRequest().');
+ }
+
+ return clone $this;
+ }
+
+ function contactUpdate($id, $contact)
+ {
+ $this->debug('contactUpdate() called');
+
+ /**
+ * Check for minimum contact data
+ */
+ if( !isset($contact['email-address']) || preg_match( "/^[A-Z0-9._%-]+@[A-Z0-9._%-]+\.[A-Z]{2,4}$/i", $contact['email-address'] ) == 0 ) {
+ $this->debug('contactUpdate() E-Mail address not provided or not a valid E-Mail address.');
+ return false;
+ }
+
+ /**
+ * Build XML POST data string for supplied contact
+ */
+ $post_data = "<person>\n";
+ while( list($k, $v) = each($contact) ) {
+ $v = str_replace("&", "&", $v);
+ $v = str_replace("'", "'", $v);
+ if (strstr($v, "|")) {
+ $mSect = explode("|", $v);
+ foreach ($mSect as $multiples) {
+ $post_data .= ' <'.trim($k).'>'.trim($multiples).'</'.trim($k).">\n";
+ }
+ } else {
+ $post_data .= ' <'.trim($k).'>'.trim($v).'</'.trim($k).">\n";
+ }
+ }
+ $post_data .= "</person>\n";
+ $this->postData = $post_data;
+ $this->debug('contactUpdate() XML POST request data<br /><table border="1"><tr><td><pre>'
+ ."\n".htmlentities($this->postData)."\n"
+ .'</pre></td></tr></table>');
+
+ /** This operation requires the "POST" Method */
+ if ($this->sendRequest('PUT', 'audiences/'.STREAMSEND_AUDIENCE.'/people/'.$id) === false) {
+ $this->debug('contactUpdate() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ var_dump($this->responseError);
+ $this->debug('contactUpdate() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('contactUpdate() Received good response from sendRequest().');
+ }
+
+ return clone $this;
+ }
+
+ /**
+ * Delete a Contact
+ *
+ * Requires a valid contact E-Mail address
+ */
+ function contactDelete( $email )
+ {
+ $this->debug('contactDelete() Called to delete E-Mail address: '.$email );
+
+ /**
+ * First find the contact to get the contact ID
+ */
+ if (!($c = $this->contactSearch( $email ))) {
+ $this->debug('contactDelete() Call to sendRequest() failed.');
+ return false;
+ } elseif ($c->responseStatus){
+ $this->debug('contactDelete() Call to sendRequest returned an error code.');
+ return false;
+ }
+
+ if( $c->contact == false ) {
+ $this->debug('contactDelete() Search for specified E-Mail address failed. Does Contact exist?');
+ return false;
+ }
+
+ $id = $c->contact->id;
+ $this->debug('contactDelete() Have an ID for this contact: '.$id);
+
+ /** Expecting only an HTTP response code, so no XML parsing is required */
+ $this->noXMLExpected = true;
+
+ /** This operation requires the "DELETE" Method */
+ if ($this->sendRequest('DELETE', 'audiences/'.STREAMSEND_AUDIENCE.'/people/'.$id) === false) {
+ $this->debug('contactDelete() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /** Be sure to reset these parameters or other functions may have trouble */
+ $this->noXMLExpected = false;
+
+ /** Process the two normally expected response codes related to deleting a contact */
+ switch( $this->responseHTTPStatus ) {
+
+ case '200':
+ $this->debug('uploadContacts() Call to sendRequest() returned a 200 OK. Contact should be deleted.');
+ return clone $this;
+ break;
+
+ case '423':
+ $this->debug('uploadContacts() Call to sendRequest() returned a 423 LOCKED. Contact cannot be deleted at this time.');
+ return clone $this;
+ break;
+
+ }
+
+ /** We get here because an unexpected HTTP response code was recieved */
+ $this->debug('uploadContacts() Call to sendRequest() returned an unexpected code. Contact may not be deleted.');
+ return false;
+ }
+
+
+ /**
+ * Lists Methods
+ *
+ * Lists are groups of contacts. People (contacts) may be subscribed to multiple lists.
+ */
+
+ function listList()
+ {
+ $this->debug('listList() called');
+
+ /** This operation requires the "GET" Method */
+ if ($this->sendRequest('GET', 'audiences/'.STREAMSEND_AUDIENCE.'/lists') === false) {
+ $this->debug('listList() Call to sendRequest() failed.');
+ return( false );
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('listList() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('listsList() Received good response from sendRequest().');
+ }
+
+ return(clone $this);
+ }
+
+
+
+ /**
+ * Memberships (subscription) Methods
+ *
+ * Membership records track to which lists each person belongs. For any given list to
+ * which a person is subscribed, a membership record exists. The creation of a membership
+ * record linking a person and a list represents the act of joining that list. The
+ * destruction of a membership record represents the act of leaving that list.
+ */
+
+ /** Not written yet */
+
+
+ /**
+ * Filters Methods
+ *
+ * Filters, along with lists, are one of two ways that the people in your audience
+ * can be grouped and manipulated. While lists are fairly static and unchanging,
+ * filters are completely dynamic. That is, no person ever "belongs" to a filter,
+ * as they might a list. Instead they are said to "match" a filter, and only when
+ * the filter is used does a clear group of people emerge.
+ */
+
+ /** Not written yet */
+
+
+ /** -------------------------------------- E-Mail Blast Methods ------------------------------------ */
+
+
+ /**
+ * Emails Methods
+ *
+ * Emails in StreamSend are those that have been saved for later use. They consist
+ * of some combination of HTML and plain text content and may be modified at any
+ * time without affecting any blasts that have used or will be using them.
+ */
+
+ /** Not written yet */
+
+
+ /**
+ * Blasts Methods
+ *
+ * A blast is the act of sending an email to one or more people in your audience. Blasts
+ * may be sent immediately or at some specified date in the future.
+ */
+
+ function blastList()
+ {
+ $this->debug('blastList() called');
+
+ /** This operation requires the "GET" Method */
+ if ($this->sendRequest('GET', 'audiences/'.STREAMSEND_AUDIENCE.'/blasts') === false) {
+ $this->debug('blastList() Call to sendRequest() failed.');
+ return( false );
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('blastList() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('blastList() Received good response from sendRequest().');
+ }
+
+ return(clone $this);
+ }
+
+ function blastGet( $id )
+ {
+ $this->debug('blastGet() called');
+
+ /** This operation requires the "GET" Method */
+ if ($this->sendRequest('GET', 'audiences/'.STREAMSEND_AUDIENCE.'/blasts/'.$id) === false) {
+ $this->debug('blastGet() Call to sendRequest() failed.');
+ return false;
+ }
+
+ /**
+ * Check if the response request was successful
+ */
+ if ($this->responseError > 0) {
+ $this->debug('blastGet() Call to sendRequest() returned an error code.');
+ } else {
+ $this->debug('blastGet() Received good response from sendRequest().');
+ }
+
+ /** Stuff the results into a logical and consistent place to use by the calling program */
+ $this->contact = $this->responseData;
+
+ return clone $this;
+ }
+
+ function monthlyStats( $month, $year )
+ {
+ global $StreamSendAccounts;
+
+ $this->debug('monthlyStats() called');
+
+ $total_accounts = $total_blasts = $total_sent = $total_delivered = $total_undelivered = $total_bounced =
+ $total_views = $total_clicks = $total_unsubscribes = $total_complaints = 0;
+
+ /**
+ * Get list of all accounts
+ */
+ if ( !($accounts = $this->accountList()) || $accounts->responseStatus ) {
+ $this->debug('monthlyStats() Unable to get account list');
+ return false;
+ }
+
+ /** Array that will contact all resulting data */
+ $account_data = array();
+
+ /**
+ * For each account
+ */
+ foreach ($accounts->responseData->account as $account) {
+
+ $a_id = (int) $account->id;
+ $account_data[$a_id] = array(
+ 'name' => (string) $account->name,
+ 'quota' => (int) $account->quota,
+ 'soft-bounce-tolerance' => (int) $account->{'soft-bounce-tolerance'},
+ 'automated-email-address' => (string) $account->{'automated-email-address'},
+ 'owner' => (string) $account->owner->{'last-name'}.', '.$account->owner->{'first-name'},
+ 'email-address' => (string) $account->owner->{'email-address'},
+ 'blasts' => 0,
+ 'sent' => 0,
+ 'delivered' => 0,
+ 'undelivered' => 0,
+ 'bounced' => 0,
+ 'views' => 0,
+ 'clicks' => 0,
+ 'unsubscribes' => 0,
+ 'complaints' => 0
+ );
+
+ $this->debug('monthlyStats() Processing account: '.$a_id);
+
+ $total_accounts++;
+
+ /**
+ * Get all blasts for the specified month
+ */
+
+ $account_data[$a_id]['blast_detail'] = array();
+
+ /** Set the account id */
+ $this->currentAccount = $a_id;
+ $this->userName = $StreamSendAccounts[$a_id]['id'];
+ $this->userPassword = $StreamSendAccounts[$a_id]['key'];
+
+ if (!($blasts = $this->blastList()) || $blasts->responseStatus ) {
+ $this->debug('monthlyStats() Unable to get blast list for account: '.$a_id);
+ $account_data[$a_id][$b_id] = false;
+ } else {
+
+ foreach ($blasts->responseData->blast as $blast) {
+
+ /** Get the scheduled date for this blast and break it up into parts */
+ $date = getdate(strtotime( $blast->{'scheduled-for'} ));
+
+ /** Is this blast scheduled for the requested month */
+ if( $date['mon'] == $month && $date['year'] == $year ) {
+
+ $account_data[$a_id]['blasts']++;
+
+ $b_id = (int) $blast->id;
+
+ $account_data[$a_id]['blast_detail'][$b_id] = array(
+ 'subject' => (string) $blast->subject,
+ 'email-address' => (string) $blast->from->{'email-address'},
+ 'scheduled-for' => (string) $blast->{'scheduled-for'},
+ 'completed-at' => (string) $blast->{'completed-at'},
+ 'status' => (string) $blast->status,
+ 'sent' => 0
+ );
+
+ /**
+ * Get blast detail
+ */
+
+ if (!($b = $this->blastGet($b_id)) || $b->responseStatus ){
+ $this->debug('monthlyStats() Unable to get blast list for account: '.$a_id);
+ } else {
+
+ $b_delivery = $b->responseData->statistics->delivery;
+ $b_activity = $b->responseData->statistics->activity;
+
+ $account_data[$a_id]['blast_detail'][$b_id]['delivered'] = (int) $b_delivery->delivered;
+ $account_data[$a_id]['blast_detail'][$b_id]['undelivered'] = (int) $b_delivery->undelivered;
+ $account_data[$a_id]['blast_detail'][$b_id]['bounced'] = (int) $b_delivery->bounced;
+ $account_data[$a_id]['blast_detail'][$b_id]['views'] = (int) $b_activity->views;
+ $account_data[$a_id]['blast_detail'][$b_id]['clicks'] = (int) $b_activity->clicks;
+ $account_data[$a_id]['blast_detail'][$b_id]['unsubscribes'] = (int) $b_activity->unsubscribes;
+ $account_data[$a_id]['blast_detail'][$b_id]['complaints'] = (int) $b_activity->complaints;
+
+ /** Calculate total number of E-Mails sent for this blast */
+ $sent = (int) $b_delivery->delivered + (int) $b_delivery->undelivered + (int) $b_delivery->bounced;
+ $account_data[$a_id]['blast_detail'][$b_id]['sent'] = $sent;
+
+ /** Add into account stats */
+ $account_data[$a_id]['blasts']++;
+ $account_data[$a_id]['sent'] += $sent;
+ $account_data[$a_id]['delivered'] += (int) $b_delivery->delivered;
+ $account_data[$a_id]['undelivered'] += (int) $b_delivery->undelivered;
+ $account_data[$a_id]['bounced'] += (int) $b_delivery->bounced;
+ $account_data[$a_id]['views'] += (int) $b_activity->views;
+ $account_data[$a_id]['clicks'] += (int) $b_activity->clicks;
+ $account_data[$a_id]['unsubscribes'] += (int) $b_activity->unsubscribes;
+ $account_data[$a_id]['complaints'] += (int) $b_activity->complaints;
+
+ /** Add into overall stats */
+ $total_blasts++;
+ $total_sent += $sent;
+ $total_delivered += (int) $b_delivery->delivered;
+ $total_undelivered += (int) $b_delivery->undelivered;
+ $total_bounced += (int) $b_delivery->bounced;
+ $total_views += (int) $b_activity->views;
+ $total_clicks += (int) $b_activity->clicks;
+ $total_unsubscribes += (int) $b_activity->unsubscribes;
+ $total_complaints += (int) $b_activity->complaints;
+
+ } // Have good blast detail
+
+ } // Check date
+
+ } // for each blast
+
+ } // Have good list of blasts for account
+
+ } // For each account
+
+ /** Stuff the results into a logical and consistent place to use by the calling program */
+ $this->stats = array(
+ 'total_blasts' => $total_blasts,
+ 'total_sent' => $total_sent,
+ 'total_delivered' => $total_delivered,
+ 'total_undelivered' => $total_undelivered,
+ 'total_bounced' => $total_bounced,
+ 'total_views' => $total_views,
+ 'total_clicks' => $total_clicks,
+ 'total_unsubscribes' => $total_unsubscribes,
+ 'total_complaints' => $total_complaints,
+ 'account_detail' => $account_data
+ );
+
+ return clone $this;
+ }
+
+
+
+ /**
+ * Bounces Methods
+ *
+ * Bounces are those instances wherein certain people do not receive a particular blast.
+ * There are many reasons for this including an invalid email address, a full inbox, a
+ * problem with the receiving email server, etc.
+ */
+
+ /** Not written yet */
+
+
+ /**
+ * Unsubcribes Methods
+ *
+ * Unsubscribes track two instances of people unsubscribing from one�s audience: manual
+ * and complaint-based.
+ */
+
+ /** Not written yet */
+
+
+ /**
+ * Clicks Methods
+ *
+ * Clicks are those instances in which a person clicks on an external link in a particular
+ * message. Because clicks are tracked by modifying hyperlink URLs in the body of the message,
+ * clicks can only be registered for those viewing the HTML portion of the message and only
+ * for those URLs that are linked via an HTML hyperlink tag.
+ */
+
+ /** Not written yet */
+
+
+ /**
+ * Views Methods
+ *
+ * Views are those instances in which a person views a particular message. Because views are
+ * tracked by way of a tracking image embedded in the message, views can only be registered
+ * for those viewing the HTML portion of the message and for whom the viewing of images is
+ * currently enabled in his or her email client.
+ */
+
+ /** Not written yet */
+
+ /** -------------------------------------- Upload and Import Methods ------------------------------------ */
+
+
+ /**
+ * Upload Method
+ *
+ * Uploads a list of contacts and import into the contact list.
+ *
+ * $contacts is an array of contacts to upload. The array should be formatted as...
+ *
+ * $contacts = array(
+ * 0 => array(
+ * 'email_address' => '{email}',
+ * '{field_name}' => '{field_value}',
+ * '{field_name}' => '{field_value}',
+ * ...
+ * ),
+ * 1 => array(
+ * 'email_address' => '{email}',
+ * '{field_name}' => '{field_value}',
+ * '{field_name}' => '{field_value}',
+ * ...
+ * ),
+ * ...
+ * );
+ *
+ * The contact data must have at least 'email_address', but can have additional
+ * fields that are setup for the account on the StreamSend side. The {field_name}
+ * for the addional field names must a "slug" for the actual name. Slugs are the
+ * field name but converted to only contain lower-case alphanumeric characters and "_".
+ *
+ * Also, all contact sub arrays must have exactly the same field names or the upload
+ * will be rejected.
+ *
+ * $reactivate re-starts the double opt-in process for the uploaded contacts that
+ * already exist in StreamSend. This is an optional parameter that defaults to false.
+ *
+ * $lists is a simple array of List ID's for Lists to which uploaded contacts should
+ * be added. This is an optional parameter.
+ *
+ */
+
+ function uploadContacts( $contacts, $reactivate = false, $lists = '' )
+ {
+ $this->debug('uploadContacts() called');
+
+ /**
+ * Compile the contacts array into a text file for upload and send it to StreamSend.
+ *
+ * This is the first step in the import process. It should result in a valid
+ * upload ID that will then be used to import the contacts.
+ */
+
+ /**
+ * Check for minimum required data
+ *
+ * Was there a list of contacts supplied?
+ */
+ if( !isset($contacts) || !is_array($contacts) || count($contacts) == 0 ) {
+ $this->debug('uploadContacts() A valid list of contacts was not provided.');
+ return false;
+ }
+
+ $this->debug('uploadContacts() '.count($contacts).' contacts provided.');
+
+ /**
+ * Get the list of valid fields for this account and convert to array with key = slug and value = id
+ */
+ if( !($fields_list = $this->fieldsList()) ) {
+ $this->debug('uploadContacts() Call to fieldsList() failed.');
+ return false;
+ }
+
+ /** Email_address is not returned in fieldsList() but is required. Curiously it doesn't have an ID like the other fields */
+ $available_fields = array( 'email_address' => 'email_address' );
+ foreach( $fields_list->responseData->field as $field ) {
+ $available_fields[(string) $field->slug] = (string) $field->id;
+ }
+
+ /**
+ * Build the text file for submission to StreamSend, "|" delimited, one contact per line.
+ * Also get the list of fields from the first contact and verify that all the rest are the same.
+ */
+ $field_names = array();
+ $upload_data = $contact_fields = $contact_data_header = $contact_data_header_sep = '';
+ $numb_contacts = $numb_fields = 0;
+ $have_email_field = false;
+
+ foreach( $contacts as $fields ) {
+
+ $field = 0;
+ $contact_data = '';
+
+ /** If this is not the first contact, make sure we have the same number of fields as the first. */
+ if( $numb_contacts > 0 && count($fields) != $numb_fields ) {
+ $this->debug('uploadContacts() Contact # '.($numb_contacts+1).' does not have the same number of fields as the first.');
+ return false;
+ }
+
+ /** For each field in this contact */
+ while( list($k, $v) = each($fields) ) {
+
+ /** If this is the first contact in the supplied array, save the field name */
+ if( $numb_contacts == 0 ) {
+ /** Check for proper field name Slug */
+ if( !isset($available_fields[$k]) ) {
+ $this->debug('uploadContacts() Improper field name Slug provided: '.$k);
+ return false;
+ }
+
+ /** Add this field to our array of field names */
+ $field_names[$field] = $k;
+
+ /** Add this field to a header that will be sent as the first line in the contact data file */
+ $contact_data_header .= $contact_data_header_sep.$k;
+
+ /** Check if this is the email_address field. If so, say that we have it */
+ if( $k == 'email_address' )
+ $have_email_field = true;
+
+ /** Compile list of contact fields for debug output */
+ $contact_fields .= $k."\n";
+
+ }
+ else
+ /** Otherwise check to see if this is the expected field name */
+ if( $field_names[$field] != $k ) {
+ $this->debug('uploadContacts() Fields for contact # '.($numb_contacts+1).' did not match first contact.');
+ return false;
+ }
+
+ /** Add field to contact, use "|" separator if it's not the first field */
+ $contact_data .= ($field>0?"\t":'').$v;
+
+ $contact_data_header_sep = "\t";
+
+ $field++;
+
+ } // for each field
+
+ /** If this is the first contact, save the number of expected fields, and set the header line for the file */
+ if( $numb_contacts == 0 ) {
+ $numb_fields = $field;
+ $upload_data = "$contact_data_header\n";
+ }
+
+ /** Add this contact to the upload data */
+ $upload_data .= "$contact_data\n";
+
+ $numb_contacts++;
+
+ } // For each Contact
+
+ /** Was the required email_address field supplied? */
+ if( !$have_email_field ) {
+ $this->debug('uploadContacts() Required "email_address" field not supplied.');
+ return false;
+ }
+ $this->debug('uploadContacts() Have required "email_address" field.');
+
+ $this->debug('uploadContacts() Contacts fields supplied...<br /><table border="1"><tr><td><pre>'
+ ."\n".htmlentities($contact_fields)
+ .'</pre></td></tr></table>');
+
+ $this->debug('uploadContacts() Contacts data supplied...<br /><table border="1"><tr><td><pre>'
+ ."\n".htmlentities($upload_data)
+ .'</pre></td></tr></table>');
+
+ /** Write the upload data to a file for Curl to send using multipart/form-data */
+ $tmpname = tempnam("/tmp", "ss_");
+ $h = fopen($tmpname, "w");
+ fwrite($h, $upload_data);
+ fclose($h);
+
+ /** Add file name of upload data to Post array */
+ $this->postData = array( 'data' => "@$tmpname" );
+
+ /** Tell sendRequest() to not bother parsing response as XML and to return response headers for parsing */
+ $this->noXMLExpected = true;
+ $this->returnHeaders = true;
+
+ /** This operation requires the "MULTIPART" Method */
+ if ($this->sendRequest('MULTIPART', 'uploads') === false) {
+ $this->debug('uploadContacts() Contacts upload call to sendRequest() failed.');
+ return false;
+ }
+
+ /** Be sure to reset these parameters or other functions may have trouble */
+ $this->noXMLExpected = false;
+ $this->returnHeaders = false;
+
+ /** Delete the temporary file */
+ unlink($tmpname);
+
+ /** We should have received a 201 response */
+ if ($this->responseHTTPStatus != '201' ) {
+ $this->debug('uploadContacts() StreamSend did not return a "201 Created" response.');
+ return false;
+ }
+
+ /** Check for the upload ID in the "Location:" header line */
+ if( !isset($this->parsedHeaders['Location']) ) {
+ $this->debug('uploadContacts() StreamSend did not return a "Location:" header with the upload ID.');
+ return false;
+ }
+
+ /** Get upload ID from "Location" header and force it to a number, then check it's not 0 */
+ if( ($upload_id = substr( strrchr( $this->parsedHeaders['Location'], '/' ), 1 ) + 0) == 0 ) {
+ $this->debug('uploadContacts() StreamSend did not return a valid upload ID. ('.$id.')');
+ return false;
+ }
+ $this->debug('uploadContacts() Have a valid upload ID. ('.$upload_id.')');
+
+ $this->responseData->uploadID = $upload_id;
+
+ /**
+ * Tell StreamSend to import the contacts supplied in the upload above.
+ *
+ * This is the second step of the process.
+ */
+
+ /** Build XML POST data string for import - Note that fields other than email_address use the field IDs here. */
+ $post_data = "<import>\n"
+ ." <reactivate>".($reactivate?'true':'false')."</reactivate>\n"
+ ." <lists>$lists</lists>\n"
+ ." <upload-id>$upload_id</upload-id>\n"
+ ." <separator>Tab</separator>\n"
+ ." <columns>\n";
+ reset( $field_names );
+ foreach( $field_names as $f ) {
+ $post_data .= " <column>".$available_fields[$f]."</column>\n";
+ }
+ $post_data .= " </columns>\n"
+ ."</import>";
+
+ $this->postData = $post_data;
+ $this->debug('uploadContacts() XML POST request data<br /><table border="1"><tr><td><pre>'
+ ."\n".htmlentities($this->postData)."\n"
+ .'</pre></td></tr></table>');
+
+ /** This operation requires the "POST" Method */
+ if ($this->sendRequest('POST', 'audiences/'.STREAMSEND_AUDIENCE.'/imports') === false) {
+ $this->debug('uploadContacts() Contact import call to sendRequest() failed.');
+ return false;
+ }
+
+ /** We should have received a 201 response */
+ if ($this->responseHTTPStatus != '201' ) {
+ $this->debug('uploadContacts() StreamSend did not return a "201 Created" response.');
+ return false;
+ }
+
+ /** Check for the upload ID in the "Location:" header line */
+ if( !isset($this->parsedHeaders['Location']) ) {
+ $this->debug('uploadContacts() StreamSend did not return a "Location:" header with import ID.');
+ return false;
+ }
+
+ /** Get upload ID from "Location" header and force it to a number, then check it's not 0 */
+ if( ($import_id = substr( strrchr( $this->parsedHeaders['Location'], '/' ), 1 ) + 0) == 0 ) {
+ $this->debug('uploadContacts() StreamSend did not return a valid upload ID. ('.$id.')');
+ return false;
+ }
+ $this->debug('uploadContacts() Have a valid import ID. ('.$import_id.')');
+
+ $this->responseData->importID = $import_id;
+
+ return clone $this;
+ }
+
+
+}
+
+?>