Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
15.56% covered (danger)
15.56%
14 / 90
12.50% covered (danger)
12.50%
1 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
BaseMessage
15.56% covered (danger)
15.56%
14 / 90
12.50% covered (danger)
12.50%
1 / 8
954.89
0.00% covered (danger)
0.00%
0 / 1
 toJSON
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromJSON
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 fromEncoded
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 fromEncodedArray
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 encode
39.39% covered (danger)
39.39%
13 / 33
0.00% covered (danger)
0.00%
0 / 1
50.62
 decode
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
132
 clearFields
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setCipherParams
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2namespace Ably\Models;
3
4use Ably\Exceptions\AblyException;
5use Ably\Utils\Crypto;
6use Ably\Log;
7
8/**
9 * Base class for messages sent over channels.
10 * Provides automatic encoding and decoding.
11 */
12abstract class BaseMessage {
13
14    /**
15     * @var string Unique ID for this message. Populated by the system.
16     */
17    public $id;
18    /**
19     * @var mixed|null The message payload.
20     */
21    public $data;
22    /**
23     * @var string|null The clientId of the client that published the message.
24     * This property is populated by the system, where the clientId is known.
25     */
26    public $clientId;
27    /**
28     * @var string|null ID of the connection.
29     * This property is populated by the system.
30     */
31    public $connectionId;
32    /**
33     * @var int The timestamp of this message. Populated by the system.
34     */
35    public $timestamp;
36    /**
37     * @var string|null Transformations to be applied to this message.
38     * If specified for new messages, it is assumed that $data is already encoded
39     * in the specified format, including any encryption.
40     * Otherwise the encoding is automatically inferred from $data.
41     */
42    public $encoding;
43    /**
44     * @var mixed Original received data, without any transformations, ignored when sending.
45     */
46    public $originalData;
47    /**
48     * @var mixed Original received encoding, ignored when sending.
49     */
50    public $originalEncoding;
51    
52    /**
53     * @var CipherParams|null Cipher parameters for encrypted messages.
54     */
55    protected $cipherParams;
56
57    /**
58     * Creates a JSON representation of this message, ready to be sent to the API.
59     *
60     * If there is an $encoding specified, the data will be left untouched.
61     * If not specified the encoding is inferred from the type of $data and any transformations
62     * such as base64 encoding for binary data and encryption are applied.
63     */
64    public function toJSON() {
65        return json_encode( $this->encode() );
66    }
67
68    /**
69     * Populates the message from JSON and automatically decodes data.
70     * @param string|stdClass $json JSON string or an already decoded object.
71     * @param bool $keepOriginal When set to true, the message won't be decoded or decrypted
72     * @throws AblyException
73     */
74    public function fromJSON( $json, $keepOriginal = false ) {
75        $this->clearFields();
76
77        if (is_object( $json )) {
78            $obj = $json;
79        } else {
80            $obj = @json_decode($json);
81            if (!$obj) {
82                throw new AblyException( 'Invalid object or JSON encoded object' );
83            }
84        }
85
86        $class = get_class( $this );
87        foreach ($obj as $key => $value) {
88            if (property_exists( $class, $key )) {
89                $this->$key = $value;
90            }
91        }
92
93        if ($keepOriginal) return;
94
95        $this->decode();
96    }
97
98    /**
99     * Creates and returns a new message from the given encoded message like object
100     * @param stdClass $obj Message-like object
101     * @param CipherParams|null $cipherParams
102     */
103    public static function fromEncoded( $obj, CipherParams $cipherParams = null ) {
104        $class = get_called_class();
105
106        $msg = new $class();
107        if ($cipherParams != null) {
108            $msg->setCipherParams( $cipherParams );
109        }
110
111        foreach ($obj as $key => $value) {
112            if (property_exists( $class, $key )) {
113                $msg->$key = $value;
114            }
115        }
116
117        $msg->decode();
118
119        return $msg;
120    }
121
122    /**
123     * Creates and returns a new message from the given encoded message like object
124     * @param array $objs Array of Message-Like objects
125     * @param CipherParams|null $cipherParams
126     */
127    public static function fromEncodedArray( $objs, CipherParams $cipherParams = null ) {
128        return array_map(
129            function( $obj ) use ($cipherParams) { return static::fromEncoded($obj, $cipherParams); },
130            $objs
131        );
132    }
133
134    /**
135     * Returns an encoded message as a stdClass ready for stringifying
136     */
137    protected function encode() {
138        $msg = new \stdClass();
139
140        if ($this->encoding) {
141            $msg->encoding = $this->encoding;
142            $msg->data = $this->data;
143
144            return $msg;
145        }
146        
147        if ($this->clientId) {
148            $msg->clientId = $this->clientId;
149        }
150
151        $isBinary = false;
152        $encodings = [];
153
154        if ( is_array( $this->data ) || $this->data instanceof \stdClass ) {
155            $encodings[] = 'json';
156            $msg->data = json_encode($this->data);
157        } else if ( is_string( $this->data ) ){
158            if ( mb_check_encoding( $this->data, 'UTF-8' ) ) { // it's a UTF-8 string
159                $msg->data = $this->data;
160            } else { // not UTF-8, assuming it's a binary string
161                $msg->data = $this->data;
162                $isBinary = true;
163            }
164        } else if ( !isset( $this->data ) || $this->data === null ) {
165            return $msg;
166        } else {
167            throw new AblyException( 'Message data must be either, string, string with binary data, JSON-encodable array or object, or null.', 40003, 400 );
168        }
169
170        if ( $this->cipherParams ) {
171            if ( !$isBinary ) {
172                $encodings[] = 'utf-8';
173            }
174
175            $msg->data = base64_encode( Crypto::encrypt( $msg->data, $this->cipherParams ) );
176            $encodings[] = 'cipher+' . $this->cipherParams->getAlgorithmString();
177            $encodings[] = 'base64';
178        } else {
179            if ( $isBinary ) {
180                $msg->data = base64_encode( $this->data );
181                $encodings[] = 'base64';
182            }
183        }
184
185        if ( count( $encodings ) ) {
186            $msg->encoding = implode( '/', $encodings );
187        } else {
188            $msg->encoding = '';
189        }
190
191        return $msg;
192    }
193
194    /**
195     * Decodes message's data field according to encoding
196     * @throws AblyException
197     */
198    protected function decode() {
199        $this->originalData = $this->data;
200        $this->originalEncoding = $this->encoding;
201
202        if (!empty( $this->encoding )) {
203            $encodings = explode( '/', $this->encoding );
204
205            foreach (array_reverse( $encodings ) as $encoding) {
206                if ($encoding == 'base64') {
207                    $this->data = base64_decode( $this->data );
208
209                    if ($this->data === false) {
210                        throw new AblyException( 'Could not base64-decode message data' );
211                    }
212
213                    array_pop( $encodings );
214                } else if ($encoding == 'json') {
215                    $this->data = json_decode( $this->data );
216
217                    if ($this->data === null) {
218                        throw new AblyException( 'Could not JSON-decode message data' );
219                    }
220
221                    array_pop( $encodings );
222                } else if (strpos( $encoding, 'cipher+' ) === 0) {
223                    if (!$this->cipherParams) {
224                        Log::e( 'Could not decrypt message data, no cipherParams provided' );
225                        break;
226                    }
227
228                    $data = Crypto::decrypt( $this->data, $this->cipherParams );
229                    
230                    if ($data === false) {
231                        Log::e( 'Could not decrypt message data' );
232                        break;
233                    }
234
235                    $this->data = $data;
236                    array_pop( $encodings );
237                }
238            }
239
240            $this->encoding = count( $encodings ) ? implode( '/', $encodings ) : null;
241        }
242    }
243
244    /**
245     * Sets all the public fields to null
246     */
247    protected function clearFields() {
248        $fields = get_object_vars( $this );
249        unset( $fields['cipherParams'] );
250
251        foreach ($fields as $key => $value) {
252            $this->$key = null;
253        }
254    }
255
256    /**
257     * Sets cipher parameters for this message for automatic encryption and decryption.
258     * @param CipherParams $cipherParams
259     */
260    public function setCipherParams( CipherParams $cipherParams ) {
261        $this->cipherParams = $cipherParams;
262    }
263}