1<?php
2/**
3 * Horde_ActiveSync_Utils::
4 *
5 * @license   http://www.horde.org/licenses/gpl GPLv2
6 *
7 * @copyright 2010-2020 Horde LLC (http://www.horde.org)
8 * @author    Michael J Rubinsky <mrubinsk@horde.org>
9 * @package   ActiveSync
10 */
11/**
12 * Horde_ActiveSync_Utils:: contains general utilities.
13 *
14 * @license   http://www.horde.org/licenses/gpl GPLv2
15 *
16 * @copyright 2010-2020 Horde LLC (http://www.horde.org)
17 * @author    Michael J Rubinsky <mrubinsk@horde.org>
18 * @package   ActiveSync
19 */
20class Horde_ActiveSync_Utils
21{
22    /**
23     * Decode a base64 encoded URI
24     *
25     * @param string $url  The Base64 encoded string.
26     *
27     * @return array  The decoded request
28     */
29    public static function decodeBase64($uri)
30    {
31        $commandMap = array(
32                0  => 'Sync',
33                1  => 'SendMail',
34                2  => 'SmartForward',
35                3  => 'SmartReply',
36                4  => 'GetAttachment',
37                9  => 'FolderSync',
38                10 => 'FolderCreate',
39                11 => 'FolderDelete',
40                12 => 'FolderUpdate',
41                13 => 'MoveItems',
42                14 => 'GetItemEstimate',
43                15 => 'MeetingResponse',
44                16 => 'Search',
45                17 => 'Settings',
46                18 => 'Ping',
47                19 => 'ItemOperations',
48                20 => 'Provision',
49                21 => 'ResolveRecipients',
50                22 => 'ValidateCert'
51            );
52        $stream = fopen('php://temp', 'r+');
53        fwrite($stream, base64_decode($uri));
54        rewind($stream);
55        $results = array();
56        // Version, command, locale
57        $data = unpack('CprotocolVersion/Ccommand/vlocale', fread($stream, 4));
58        $results['ProtVer'] = substr($data['protocolVersion'], 0, -1) . '.' . substr($data['protocolVersion'], -1);
59        $results['Cmd'] = $commandMap[$data['command']];
60        $results['Locale'] = $data['locale'];
61
62        // deviceId
63        $length = ord(fread($stream, 1));
64        if ($length > 0) {
65            $data = fread($stream, $length);
66            $data = unpack('H' . ($length * 2) . 'DevID', $data);
67            $results['DeviceId'] = $data['DevID'];
68        }
69
70        // policyKey
71        $length = ord(fread($stream, 1));
72        if ($length > 0) {
73            $data  = unpack('VpolicyKey', fread($stream, $length));
74            $results['PolicyKey'] = $data['policyKey'];
75        }
76
77        // deviceType
78        $length = ord(fread($stream, 1));
79        if ($length > 0) {
80            $data  = unpack('A' . $length . 'devType', fread($stream, $length));
81            $results['DeviceType'] = $data['devType'];
82        }
83
84        // Remaining properties
85        while (!feof($stream)) {
86            $tag = ord(fread($stream, 1));
87            $length = ord(fread($stream, 1));
88            if ($length > 0 || $tag == 7) {
89                switch ($tag) {
90                case 0:
91                    $data = unpack('A' . $length . 'AttName', fread($stream, $length));
92                    $results['AttachmentName'] = $data['AttName'];
93                    break;
94                case 1:
95                    $data = unpack('A' . $length . 'CollId', fread($stream, $length));
96                    $results['CollectionId'] = $data['CollId'];
97                    break;
98                case 3:
99                    $data = unpack('A' . $length . 'ItemId', fread($stream, $length));
100                    $results['ItemId'] = $data['ItemId'];
101                    break;
102                case 4:
103                    $data = unpack('A' . $length . 'Lid', fread($stream, $length));
104                    $results['LongId'] = $data['Lid'];
105                    break;
106                case 5:
107                    $data = unpack('A' . $length . 'Pid', fread($stream, $length));
108                    $results['ParentId'] = $data['Pid'];
109                    break;
110                case 6:
111                    $data = unpack('A' . $length . 'Oc', fread($stream, $length));
112                    $results['Occurrence'] = $data['Oc'];
113                    break;
114                case 7:
115                    $options = ord(fread($stream, 1));
116                    $results['SaveInSent'] = !!($options & 0x01);
117                    $results['AcceptMultiPart'] = !!($options & 0x02);
118                    break;
119                case 8:
120                    $data = unpack('A' . $length . 'User', fread($stream, $length));
121                    $results['User'] = $data['User'];
122                    break;
123                }
124            }
125        }
126
127        return $results;
128    }
129
130    /**
131     * Obtain the UID from a MAPI GOID.
132     *
133     * See http://msdn.microsoft.com/en-us/library/hh338153%28v=exchg.80%29.aspx
134     *
135     * @param string $goid  Base64 encoded Global Object Identifier.
136     *
137     * @return string  The UID
138     * @deprecated  Will be removed in H6. Use Horde_Mapi::getUidFromGoid
139     */
140    public static function getUidFromGoid($goid)
141    {
142        $goid = base64_decode($goid);
143
144        // First, see if it's an Outlook UID or not.
145        if (substr($goid, 40, 8) == 'vCal-Uid') {
146            // For vCal UID values:
147            // Bytes 37 - 40 contain length of data and padding
148            // Bytes 41 - 48 are == vCal-Uid
149            // Bytes 53 until next to the last byte (/0) contain the UID.
150            return trim(substr($goid, 52, strlen($goid) - 1));
151        } else {
152            // If it's not a vCal UID, then it is Outlook style UID:
153            // The entire decoded goid is converted to hex representation with
154            // bytes 17 - 20 converted to zero
155            $hex = array();
156            foreach (str_split($goid) as $chr) {
157                $hex[] = sprintf('%02X', ord($chr));
158            }
159            array_splice($hex, 16, 4, array('00', '00', '00', '00'));
160            return implode('', $hex);
161        }
162    }
163
164    /**
165     * Create a MAPI GOID from a UID
166     * See http://msdn.microsoft.com/en-us/library/ee157690%28v=exchg.80%29
167     *
168     * @param string $uid  The UID value to encode.
169     *
170     * @return string  A Base64 encoded GOID
171     * @deprecated  Will be removed in H6. Use Horde_Mapi::createGoid
172     */
173    public static function createGoid($uid, $options = array())
174    {
175        // Bytes 1 - 16 MUST be equal to the GOID identifier:
176        $arrayid = '040000008200E00074C5B7101A82E008';
177
178        // Bytes 17 - 20 - Exception replace time (YH YL M D)
179        $exception = '00000000';
180
181        // Bytes 21 - 28 The 8 byte creation time (can be all zeros if not available).
182        $creationtime = '0000000000000000';
183
184        // Bytes 29 - 36 Reserved 8 bytes must be all zeros.
185        $reserved = '0000000000000000';
186
187        // Bytes 37 - 40 - A long value describing the size of the UID data.
188        $size = strlen($uid);
189
190        // Bytes 41 - 52 - MUST BE vCal-Uid 0x01 0x00 0x00 0x00
191        $vCard = '7643616C2D55696401000000';
192
193        // The UID Data:
194        $hexuid = '';
195        foreach (str_split($uid) as $chr) {
196            $hexuid .= sprintf('%02X', ord($chr));
197        }
198
199        // Pack it
200        $goid = pack('H*H*H*H*VH*H*x', $arrayid, $exception, $creationtime, $reserved, $size, $vCard, $hexuid);
201
202        return base64_encode($goid);
203    }
204
205    /**
206     * Ensure $data is converted to valid UTF-8 data. Works as follows:
207     * Converts to UTF-8, assuming data is in $from_charset encoding. If
208     * that produces invalid UTF-8, attempt to convert to most common mulitibyte
209     * encodings. If that *still* fails, strip out non 7-Bit characters...and
210     * force encoding to UTF-8 from $from_charset as a last resort.
211     *
212     * @param string $data          The string data to convert to UTF-8.
213     * @param string $from_charset  The character set to assume $data is encoded
214     *                              in.
215     *
216     * @return string  A valid UTF-8 encoded string.
217     */
218    public static function ensureUtf8($data, $from_charset)
219    {
220        $text = Horde_String::convertCharset($data, $from_charset, 'UTF-8');
221        if (!Horde_String::validUtf8($text)) {
222            $test_charsets = array(
223                'windows-1252',
224                'UTF-8'
225            );
226            foreach ($test_charsets as $charset) {
227                if ($charset != $from_charset) {
228                    $text = Horde_String::convertCharset($data, $charset, 'UTF-8');
229                    if (Horde_String::validUtf8($text)) {
230                        return $text;
231                    }
232                }
233            }
234            // Invalid UTF-8 still found. Strip out non 7-bit characters, or if
235            // that fails, force a conversion to UTF-8 as a last resort. Need
236            // to break string into smaller chunks to avoid hitting
237            // https://bugs.php.net/bug.php?id=37793
238            $chunk_size = 4000;
239            $text = '';
240            while ($data !== false && strlen($data)) {
241                $test = self::_stripNon7BitChars(substr($data, 0, $chunk_size));
242                if ($test !== false) {
243                    $text .= $test;
244                } else {
245                    return Horde_String::convertCharset($data, $from_charset, 'UTF-8', true);
246                }
247                $data = substr($data, $chunk_size);
248            }
249        }
250
251        return $text;
252    }
253
254    /**
255     * Strip out non 7Bit characters from a text string.
256     *
257     * @param string $text  The string to strip.
258     *
259     * @return string|boolean  The stripped string, or false if failed.
260     */
261    protected static function _stripNon7BitChars($text)
262    {
263        return preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', '', $text);
264    }
265
266}
267