Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 76
XmlTransactionProcessor
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 7
342
0.00% covered (danger)
0.00%
0 / 76
 setEndpointURL
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 setTimeout
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 setTransactionMap
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 setEnvelope
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 doTransaction
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 37
 constructDomFromArray
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 14
 doHttpTransaction
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 17
<?php
/**
 * -- License --
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 */
/**
 * Class that prepares an XML transaction and executes it. Right now it's probably way specific
 * to the SilverPop method. Hopefully easy to extend if required?
 */
class XmlTransactionProcessor {
    /** @var array[] */
    private $mTransactionMap = [];
    /** @var array */
    private $mEnvelope = [];
    /** @var string|int */
    private $mTimeout = 'default';
    /** @var string */
    private $mURL = '';
    /**
     * Set the remote server we will be connecting too
     * @param string $url
     */
    public function setEndpointURL( $url ) {
        $this->mURL = $url;
    }
    /**
     * Maximum cURL operation time
     * @param int $timeout
     */
    public function setTimeout( $timeout ) {
        $this->mTimeout = $timeout;
    }
    /**
     * The transaction map is an array, keyed on transaction name. Each node should have two
     * leaves: 'in' and 'out'. Out should be an array of 'key' which is the XML node name, and 'value'
     * which is the name of the key in the data structure passed to doTransaction(). 'In' should
     * be a function name that will process the return data. The 'in' function will be called in
     * the context of a callbackObj - see doTransaction().
     *
     * In functions should accept three arguments:
     *  - string        : Transaction name
     *  - DOMDocument   : The returned XML object
     *  - this          : In case the function needs to modify this processor object
     *
     * @param array[] $map
     */
    public function setTransactionMap( array $map ) {
        $this->mTransactionMap = $map;
    }
    /**
     * Wrap the output XML in more nodes! Each entry in the envelope is a new wrapping element.
     * @param array $envelope
     */
    public function setEnvelope( array $envelope ) {
        $this->mEnvelope = $envelope;
    }
    /**
     * Performs an XML transaction!
     *
     * @param string $txnName Name of transaction in transaction map
     * @param mixed $callbackObj Reference to class that holds the callback function
     * @param array $outParams Map of parameter data
     *
     * @return bool If true, the transaction was completely successful.
     */
    public function doTransaction( $txnName, $callbackObj, $outParams = [] ) {
        global $wgFundraisingEmailUnsubscribeLogXmlTransactions;
        Logger::pushLabel( 'XMLTransaction' );
        $retval = false;
        // Brief sanity check
        if ( !array_key_exists( $txnName, $this->mTransactionMap ) ) {
            Logger::log( "Transaction '$txnName' does not exist in map!", LOG_ERR, 'XMLTransaction' );
        // And a further check to ensure that the processing callback function does exist
        } elseif ( !method_exists( $callbackObj, $this->mTransactionMap[$txnName]['in'] ) ) {
            $errstr = "Transaction '$txnName' specifies callback function that does not exist in provided object!";
            Logger::log( $errstr, LOG_ERR, 'XMLTransaction' );
            throw new MWException( $errstr );
        // We're sane; process the data
        } else {
            // Construct the DOM tree
            $dom = new DOMDocument( '1.0', 'utf-8' );
            $root = $dom;
            foreach ( $this->mEnvelope as $element ) {
                $el = new DOMElement( $element );
                $root->appendChild( $el );
                $root = $el;
            }
            $el = new DOMElement( $txnName );
            $root->appendChild( $el );
            $root = $el;
            $this->constructDomFromArray( $this->mTransactionMap[$txnName]['out'], $outParams, $root );
            // Send it!
            Logger::log( "Sending transaction '$txnName'" );
            if ( $wgFundraisingEmailUnsubscribeLogXmlTransactions ) {
                Logger::log( "Transaction content: " . $dom->saveXML() );
            }
            $retData = $this->doHttpTransaction( $dom->saveXML(), $this->mTimeout );
            // Call the processing hook if everything was OK
            $retDOM = new DOMDocument();
            if ( ( $retData != false ) && ( $retDOM->loadXML( $retData ) ) ) {
                $function = $this->mTransactionMap[$txnName]['in'];
                $retval = $callbackObj->$function( $txnName, $retDOM, $this );
            } else {
                Logger::log( "Server did not return useful content: " . json_encode( $retData ) );
            }
        }
        Logger::popLabel();
        return $retval;
    }
    /**
     * Helper function which will create a DOM tree based on an array tree. Will lookup key data
     * in the $data array.
     *
     * @param array $domArray The starting DOM array -- which is not a DOM object
     * @param array $data The source parameter data
     * @param DOMElement|DOMNode &$dom The resulting DOM object
     *
     * @throws MWException
     */
    protected function constructDomFromArray( array $domArray, $data, &$dom ) {
        foreach ( $domArray as $element => $value ) {
            if ( is_array( $value ) ) {
                $el = new DOMElement( $element );
                $this->constructDomFromArray( $value, $data, $el );
            } else {
                if ( array_key_exists( $value, $data ) ) {
                    $el = new DOMElement( $element, (string)$data[$value] );
                } else {
                    throw new MWException( "Data key '$value' does not exist! Node '$element' cannot be created." );
                }
            }
            $dom->appendChild( $el );
        }
    }
    /**
     * Actually perform the HTTP transaction.
     *
     * @param string|array $data
     * @param string $timeout
     *
     * @return bool|string
     */
    protected function doHttpTransaction( $data, $timeout = 'default' ) {
        $options['method'] = 'POST';
        $options['timeout'] = $this->mTimeout;
        $req = MWHttpRequest::factory( $this->mURL, $options );
        if ( !is_array( $data ) ) {
            $req->setHeader( 'Content-Type', 'text/xml; charset=utf8' );
            $req->setHeader( 'Content-Length', strlen( $data ) );
        }
        $req->setData( $data );
        $status = $req->execute();
        if ( $status->isOK() ) {
            return $req->getContent();
        } else {
            $errors = json_encode( [ 'curl' => $status->errors, 'request' => $req->status->getErrorsArray() ] );
            Logger::log( "Communications failed with : $errors", LOG_ERR, 'XMLTransaction' );
            return false;
        }
    }
}