1: <?php
2: /**
3: * The Horde_SyncMl_Command_Sync class provides a SyncML implementation of the
4: * Sync command as defined in SyncML Representation Protocol, version 1.1,
5: * section 5.5.15.
6: *
7: * The Sync command is used to indicate a data synchronization operation. The
8: * command handler for the Sync command is the central class to dispatch sync
9: * messages.
10: *
11: * During parsing of the received XML, the actual sync commands (Add, Replace,
12: * Delete) from the client are stored in the $_syncElements attribute. When
13: * the output method of Horde_SyncMl_Command_Sync is called, these elements are
14: * processed and the resulting status messages created. Then the server
15: * modifications are sent back to the client by the handleSync() method which
16: * is called from within the output method.
17: *
18: * Copyright 2005-2012 Horde LLC (http://www.horde.org/)
19: *
20: * See the enclosed file COPYING for license information (LGPL). If you
21: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
22: *
23: * @author Karsten Fourmont <karsten@horde.org>
24: * @author Jan Schneider <jan@horde.org>
25: * @package SyncMl
26: */
27: class Horde_SyncMl_Command_Sync extends Horde_SyncMl_Command
28: {
29: /**
30: * Name of the command.
31: *
32: * @var string
33: */
34: protected $_cmdName = 'Sync';
35:
36: /**
37: * Source database of the <Sync> command.
38: *
39: * @var string
40: */
41: protected $_sourceURI;
42:
43: /**
44: * Target database of the <Sync> command.
45: *
46: * @var string
47: */
48: protected $_targetURI;
49:
50: /**
51: * Horde_SyncMl_SyncElement object for the currently parsed sync command.
52: *
53: * @var Horde_SyncMl_SyncElement
54: */
55: protected $_curItem;
56:
57: /**
58: * List of all Horde_SyncMl_SyncElement objects that have parsed.
59: *
60: * @var array
61: */
62: protected $_syncElements = array();
63:
64: /**
65: * The MIME content type of the currently parsed sync command as specified
66: * by the <Type> element inside a <Meta> section.
67: *
68: * @var string
69: */
70: protected $_contentType = 'text/plain';
71:
72: /**
73: * Encoding format of the content as specified in the <Meta><Format>
74: * element, like 'b64'.
75: *
76: * @var string
77: */
78: protected $_contentFormat = 'chr';
79:
80: /**
81: * The command ID (<CmdID>) of the currently parsed sync command.
82: *
83: * This is different from the command ID of the <Sync> command itself.
84: *
85: * @var integer
86: */
87: protected $_itemCmdID;
88:
89: /**
90: * Name of the currently parsed sync command, like 'Add'.
91: *
92: * @var string
93: */
94: protected $_elementType;
95:
96: /**
97: * Whether a <MoreData> element has indicated that the sync command is
98: * split into several SyncML message chunks.
99: *
100: * @var boolean
101: */
102: protected $_itemMoreData;
103:
104: /**
105: * The size of the data item of the currently parsed sync command in bytes
106: * as specified by a <Size> element.
107: *
108: * @var integer
109: */
110: protected $_itemSize;
111:
112: /**
113: * Start element handler for the XML parser, delegated from
114: * Horde_SyncMl_ContentHandler::startElement().
115: *
116: * @param string $uri The namespace URI of the element.
117: * @param string $element The element tag name.
118: * @param array $attrs A hash with the element's attributes.
119: */
120: public function startElement($uri, $element, $attrs)
121: {
122: parent::startElement($uri, $element, $attrs);
123: $state = $GLOBALS['backend']->state;
124:
125: switch (count($this->_stack)) {
126: case 2:
127: if ($element == 'Replace' ||
128: $element == 'Add' ||
129: $element == 'Delete') {
130: $this->_contentType = 'text/plain';
131: $this->_elementType = $element;
132: $this->_itemSize = null;
133: }
134: break;
135:
136: case 3:
137: if ($element == 'Item') {
138: if (isset($state->curSyncItem)) {
139: // Copy from state in case of <MoreData>.
140: $this->_curItem = $state->curSyncItem;
141: // Set CmdID to the current CmdId, not the initial one
142: // from the first message.
143: $this->_curItem->cmdID = $this->_itemCmdID;
144: } else {
145: $this->_curItem = new Horde_SyncMl_SyncElement(
146: $state->getSync($this->_targetURI),
147: $this->_elementType,
148: $this->_itemCmdID,
149: $this->_itemSize);
150: }
151: $this->_itemMoreData = false;
152: }
153: }
154: }
155:
156: /**
157: * End element handler for the XML parser, delegated from
158: * Horde_SyncMl_ContentHandler::endElement().
159: *
160: * @param string $uri The namespace URI of the element.
161: * @param string $element The element tag name.
162: */
163: public function endElement($uri, $element)
164: {
165: switch (count($this->_stack)) {
166: case 3:
167: switch ($element) {
168: case 'LocURI':
169: if (!isset($this->_currentSyncElement)) {
170: if ($this->_stack[1] == 'Source') {
171: $this->_sourceURI = trim($this->_chars);
172: } elseif ($this->_stack[1] == 'Target') {
173: $this->_targetURI = trim($this->_chars);
174: }
175: }
176: break;
177:
178: case 'Item':
179: if ($this->_itemMoreData) {
180: // Store to continue in next session.
181: $GLOBALS['backend']->state->curSyncItem = $this->_curItem;
182: } else {
183: // Finished. Store to syncElements[].
184: if (empty($this->_curItem->contentType)) {
185: $this->_curItem->contentType = $this->_contentType;
186: }
187: if (empty($this->_curItem->contentFormat)) {
188: $this->_curItem->contentFormat = $this->_contentFormat;
189: }
190:
191: $this->_syncElements[] = $this->_curItem;
192: // @todo: check if size matches strlen(content) when
193: // size>0, esp. in case of <MoreData>.
194:
195: // Unset the saved state item if it was not unset in
196: // endElement().
197: if (isset($GLOBALS['backend']->state->curSyncItem)) {
198: unset($GLOBALS['backend']->state->curSyncItem);
199: }
200:
201: unset($this->_curItem);
202: }
203: break;
204:
205: case 'CmdID':
206: $this->_itemCmdID = trim($this->_chars);
207: break;
208: }
209: break;
210:
211: case 4:
212: switch ($element) {
213: case 'Format':
214: if ($this->_stack[2] == 'Meta') {
215: $this->_contentFormat = trim($this->_chars);
216: }
217: break;
218: case 'Type':
219: if ($this->_stack[2] == 'Meta') {
220: $this->_contentType = trim($this->_chars);
221: }
222: break;
223: case 'Data':
224: // Trim only if we had a MoreData tag before to not corrupt
225: // pictures.
226: if (isset($GLOBALS['backend']->state->curSyncItem)) {
227: $this->_curItem->content .= ltrim($this->_chars);
228: unset($GLOBALS['backend']->state->curSyncItem);
229: } else {
230: // Don't trim, because we have to check the raw content's
231: // size.
232: $this->_curItem->content .= $this->_chars;
233: }
234: break;
235: case 'MoreData':
236: $this->_itemMoreData = true;
237: break;
238: case 'Size':
239: $this->_itemSize = $this->_chars;
240: break;
241: }
242: break;
243:
244: case 5:
245: switch ($element) {
246: case 'LocURI':
247: if ($this->_stack[3] == 'Source') {
248: $this->_curItem->cuid = trim($this->_chars);
249: } elseif ($this->_stack[3] == 'Target') {
250: // Not used: we ignore "suid proposals" from client.
251: }
252: break;
253:
254: case 'Format':
255: if ($this->_stack[3] == 'Meta') {
256: $this->_curItem->contentFormat = trim($this->_chars);
257: }
258: break;
259:
260: case 'Type':
261: $this->_curItem->contentType = trim($this->_chars);
262: break;
263: }
264: break;
265:
266: case 6:
267: if ($element == 'Type') {
268: $this->_curItem->contentType = trim($this->_chars);
269: }
270: break;
271: }
272:
273: parent::endElement($uri, $element);
274: }
275:
276: /**
277: * Implements the actual business logic of the Sync command.
278: */
279: public function handleCommand($debug = false)
280: {
281: $state = $GLOBALS['backend']->state;
282:
283: // Handle unauthenticated first.
284: if (!$state->authenticated) {
285: $this->_outputHandler->outputStatus($this->_cmdID, $this->_cmdName,
286: Horde_SyncMl::RESPONSE_INVALID_CREDENTIALS);
287: return;
288: }
289:
290: if ($debug) {
291: $sync = &$state->getSync($this->_targetURI);
292: $sync = new Horde_SyncMl_Sync(Horde_SyncMl::ALERT_TWO_WAY,
293: $this->_targetURI,
294: $this->_sourceURI,
295: 0, 0, 0);
296: } else {
297: $sync = &$state->getSync($this->_targetURI);
298: $sync->addSyncReceived();
299:
300: if (!is_object($sync)) {
301: $GLOBALS['backend']->logMessage(
302: 'No sync object found for URI ' . $this->_targetURI, 'ERR');
303: // @todo: create meaningful status code here.
304: }
305: }
306:
307: /* @todo: Check: do we send a status for every sync or only once after
308: * one sync is completed?
309: * SE K750 expects Status response to be sent before Sync output
310: * by server is produced. */
311: $this->_outputHandler->outputStatus($this->_cmdID, $this->_cmdName,
312: Horde_SyncMl::RESPONSE_OK,
313: $this->_targetURI,
314: $this->_sourceURI);
315:
316: // Here's where client modifications are processed.
317: $device = $state->getDevice();
318: $omit = $device->omitIndividualSyncStatus();
319: foreach ($this->_syncElements as $item) {
320: $result = $sync->handleClientSyncItem($this->_outputHandler, $item);
321: if (!$omit) {
322: $this->_outputStatus($item);
323: }
324: }
325:
326: if ($this->_itemMoreData) {
327: // Last item had <MoreData> element, produce appropriate response.
328: $this->_outputHandler->outputStatus(
329: $state->curSyncItem->cmdID,
330: $state->curSyncItem->elementType,
331: Horde_SyncMl::RESPONSE_CHUNKED_ITEM_ACCEPTED_AND_BUFFERED,
332: '',
333: $state->curSyncItem->cuid);
334: // @todo: check if we have to send Alert NEXT_MESSAGE here!
335: }
336: }
337:
338: /**
339: * Creates the <Status> response for one Add|Replace|Delete SyncElement.
340: *
341: * @param Horde_SyncMl_SyncElement $element The element for which the status is
342: * to be created.
343: */
344: protected function _outputStatus($element)
345: {
346: // @todo: produce valid status
347: $this->_outputHandler->outputStatus($element->cmdID,
348: $element->elementType,
349: $element->responseCode,
350: '',
351: $element->cuid);
352: }
353: }
354: