Overview

Packages

  • None
  • SyncMl

Classes

  • Horde_SyncMl
  • Horde_SyncMl_Backend
  • Horde_SyncMl_Backend_Horde
  • Horde_SyncMl_Command
  • Horde_SyncMl_Command_Alert
  • Horde_SyncMl_Command_Final
  • Horde_SyncMl_Command_Get
  • Horde_SyncMl_Command_Map
  • Horde_SyncMl_Command_Put
  • Horde_SyncMl_Command_Replace
  • Horde_SyncMl_Command_Results
  • Horde_SyncMl_Command_Status
  • Horde_SyncMl_Command_Sync
  • Horde_SyncMl_Command_SyncHdr
  • Horde_SyncMl_ContentHandler
  • Horde_SyncMl_DataStore
  • Horde_SyncMl_Device
  • Horde_SyncMl_Device_Nokia
  • Horde_SyncMl_Device_P800
  • Horde_SyncMl_Device_sync4j
  • Horde_SyncMl_Device_Sync4JMozilla
  • Horde_SyncMl_Device_Synthesis
  • Horde_SyncMl_DeviceInfo
  • Horde_SyncMl_Property
  • Horde_SyncMl_PropertyParameter
  • Horde_SyncMl_State
  • Horde_SyncMl_Sync
  • Horde_SyncMl_SyncElement
  • Horde_SyncMl_Translation
  • Horde_SyncMl_XmlOutput
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * There is one global object used by SyncML:
  4:  * 1) $GLOBALS['backend']
  5:  *    Backend to handle the communication with the datastore.
  6:  *
  7:  * @todo: Main Todos:
  8:  * - ensure that no server data is written for Horde_SyncMl::ALERT_ONE_WAY_FROM_SERVER
  9:  *   even when client sends data (security!)
 10:  * - consinstant naming of clientSyncDB (currently called targetLocURI, db
 11:  *   or synctype)
 12:  * - tackle the AddReplace issue: when a Replace is issued (i.e. during
 13:  *   SlowSync) the server should first check if the entry already exists.
 14:  *   Like: does a calendar entry with the same timeframe, same subject and
 15:  *   location exist. If so, the replace should replace this value rather than
 16:  *   create a new one as a duplicate.
 17:  *
 18:  * Copyright 2003-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  Anthony Mills <amills@pyramid6.com>
 25:  * @package SyncMl
 26:  */
 27: class Horde_SyncMl_ContentHandler
 28: {
 29:     /**
 30:      * Stack for holding the xml elements during creation of the object from
 31:      * the xml event flow.
 32:      *
 33:      * @var array
 34:      */
 35:     protected $_Stack = array();
 36: 
 37:     /**
 38:      * @var string
 39:      */
 40:     protected $_chars;
 41: 
 42:     /**
 43:      * Instance of Horde_SyncMl_Command. Events are passed through to this
 44:      * ContentHandler.
 45:      *
 46:      * @var Horde_SyncMl_Command
 47:      */
 48:     protected $_currentCommand;
 49: 
 50:     /**
 51:      * Whether we received a final element in this message.
 52:      */
 53:     protected $_gotFinal = false;
 54: 
 55:     protected $_xmlWriter;
 56: 
 57:     protected $_wbxmlparser = null;
 58: 
 59:     /**
 60:      * The response URI as sent by the server.
 61:      *
 62:      * This is the endpoint URL of the RPC server.
 63:      *
 64:      * @var string
 65:      */
 66:     protected $_respURI;
 67: 
 68:     public $debug = false;
 69: 
 70:     public function __construct()
 71:     {
 72:         /* Set to true to indicate that we expect another message from the
 73:          * client. If this is still false at the end of processing, the sync
 74:          * session is finished and we can close the session. */
 75:         $GLOBALS['message_expectresponse'] = false;
 76:     }
 77: 
 78:     /**
 79:      * Here's were all the processing takes place: gets the SyncML request
 80:      * data and returns a SyncML response. The only thing that needs to be in
 81:      * place before invoking this public function is a working backend.
 82:      *
 83:      * @param string $request      The raw request string.
 84:      * @param string $contentType  The MIME content type of the request. Should
 85:      *                             be either application/vnd.syncml or
 86:      *                             application/vnd.syncml+wbxml.
 87:      * @param string $respURI      The url of the server endpoint. Will be
 88:      *                             returned in the RespURI element.
 89:      */
 90:     public function process($request, $contentType, $respURI = null)
 91:     {
 92:         $isWBXML = $contentType =='application/vnd.syncml+wbxml';
 93:         $this->_respURI = $respURI;
 94: 
 95:         /* Catch any errors/warnings/notices that may get thrown while
 96:          * processing. Don't want to let anything go to the client that's not
 97:          * part of the valid response. */
 98:         ob_start();
 99: 
100:         $GLOBALS['backend']->logFile(Horde_SyncMl_Backend::LOGFILE_CLIENTMESSAGE, $request, $isWBXML);
101: 
102:         if (!$isWBXML) {
103:             /* XML code. */
104: 
105:             /* try to extract charset from XML text */
106:             if (preg_match('/^\s*<\?xml[^>]*encoding\s*=\s*"([^"]*)"/i',
107:                            $request, $m)) {
108:                 $charset = $m[1];
109:             } else {
110:                 $charset = 'UTF-8';
111:             }
112: 
113:             $GLOBALS['backend']->setCharset($charset);
114: 
115:             /* Init output handler. */
116:             $this->_xmlWriter = &Horde_SyncMl_XmlOutput::singleton();
117:             /* Horde_Xml_Wbxml_ContentHandler Is a class that produces plain XML
118:              * output. */
119:             $this->_xmlWriter->init(new Horde_Xml_Wbxml_ContentHandler());
120: 
121:             /* Create the XML parser and set method references. */
122:             $parser = xml_parser_create_ns($charset);
123:             xml_set_object($parser, $this);
124:             xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
125:             xml_set_element_handler($parser, '_startElement', '_endElement');
126:             xml_set_character_data_handler($parser, '_characters');
127:             xml_set_processing_instruction_handler($parser, '');
128:             xml_set_external_entity_ref_handler($parser, '');
129: 
130:             /* Here we go: fire off events: */
131:             if (!xml_parse($parser, $request)) {
132:                 $s = sprintf('XML error: %s at line %d',
133:                              xml_error_string(xml_get_error_code($parser)),
134:                              xml_get_current_line_number($parser));
135:                 $GLOBALS['backend']->logMessage($s, 'ERR');
136:                 xml_parser_free($parser);
137:                 return new PEAR_Error($s);
138:             }
139: 
140:             xml_parser_free($parser);
141: 
142:         } else {
143:             /* The decoder works like the parser in the XML code above: It
144:              * parses the input and calls the callback functions of $this. */
145:             $this->_wbxmlparser = new Horde_Xml_Wbxml_Decoder();
146:             $this->_wbxmlparser->setContentHandler($this);
147: 
148:             /* Init output handler. */
149:             $this->_xmlWriter = &Horde_SyncMl_XmlOutput::singleton();
150:             $this->_xmlWriter->init(new Horde_Xml_Wbxml_Encoder());
151: 
152:             /* Here we go: fire off events: */
153:             /* @todo catch exceptions */
154:             $this->_wbxmlparser->decode($request);
155:         }
156: 
157:         $id = @session_id();
158:         $sessionclose = empty($id);
159: 
160:         $output = $this->getOutput();
161:         if (!$isWBXML) {
162:             $output = '<?xml version="1.0" encoding="' . $charset . '"?>' . $output;
163:         }
164:         $GLOBALS['backend']->logFile(Horde_SyncMl_Backend::LOGFILE_SERVERMESSAGE, $output, $isWBXML, $sessionclose);
165: 
166:         /* Clear the output buffer that we started above, and log anything
167:          * that came up for later debugging. */
168:         $errorLogging = ob_get_clean();
169: 
170:         if (!empty($errorLogging)) {
171:             $GLOBALS['backend']->logMessage('Caught output: ' . $errorLogging, 'WARN');
172:         }
173: 
174:         return $output;
175:     }
176: 
177:     /*
178:      * CONTENTHANDLER CALLBACK FUNCTIONS
179:      * The following functions are callback functions that are called by the
180:      * XML parser. The XML and WBXML parsers use slightly different functions,
181:      * so the methods are duplicated.
182:      */
183: 
184:     /**
185:      * Returns the XML|WBXML output once processing is finished.
186:      *
187:      * @return string  The XML or WBXML output data.
188:      */
189:     public function getOutput()
190:     {
191:         return $this->_xmlWriter->getOutput();
192:     }
193: 
194:     /**
195:      * Callback public function called by XML parser.
196:      */
197:     protected function _startElement($parser, $tag, $attributes)
198:     {
199:         list($uri, $name) = $this->_splitURI($tag);
200:         $this->startElement($uri, $name, $attributes);
201:     }
202: 
203:     /**
204:      * Callback public function called by XML parser.
205:      */
206:     protected function _characters($parser, $chars)
207:     {
208:         $this->characters($chars);
209:     }
210: 
211:     /**
212:      * Callback public function called by XML parser.
213:      */
214:     protected function _endElement($parser, $tag)
215:     {
216:         list($uri, $name) = $this->_splitURI($tag);
217:         $this->endElement($uri, $name);
218:     }
219: 
220:     /**
221:      * Splits an URI as provided by the XML parser.
222:      */
223:     protected function _splitURI($tag)
224:     {
225:         $parts = explode(':', $tag);
226:         $name = array_pop($parts);
227:         $uri = implode(':', $parts);
228:         return array($uri, $name);
229:     }
230: 
231:     /**
232:      * Callback public function called by WBXML parser.
233:      */
234:     public function startElement($uri, $element, $attrs)
235:     {
236:         $this->_Stack[] = $element;
237: 
238:         // <SyncML>: don't do anyhting yet
239:         if (count($this->_Stack) == 1) {
240:             return;
241:         }
242: 
243:         // header or body?
244:         if ($this->_Stack[1] == 'SyncHdr') {
245:             if (count($this->_Stack) == 2) {
246:                 $this->_currentCommand = new Horde_SyncMl_Command_SyncHdr($this->_xmlWriter);
247:             }
248:             $this->_currentCommand->startElement($uri, $element, $attrs);
249:         } else {
250:             switch (count($this->_Stack)) {
251:             case 2:
252:                  // <SyncBody>: do nothing yet
253:                  break;
254:             case 3:
255:                 // new Command:
256:                 // <SyncML><SyncBody><[Command]>
257:                 $this->_currentCommand = &Horde_SyncMl_Command::factory($element,$this->_xmlWriter);
258:                 $this->_currentCommand->startElement($uri, $element, $attrs);
259:                 break;
260:             default:
261:                 // pass on to current command handler:
262:                 // <SyncML><SyncBody><Command><...>
263:                 $this->_currentCommand->startElement($uri, $element, $attrs);
264:                 break;
265:             }
266:         }
267:     }
268: 
269:     /**
270:      * Callback public function called by WBXML parser.
271:      */
272:     public function endElement($uri, $element)
273:     {
274:         // </SyncML>: everything done already by end of SyncBody
275:         if (count($this->_Stack) == 1) {
276:             return;
277:         }
278:         // header or body?
279:         if ($this->_Stack[1] == 'SyncHdr') {
280:             switch (count($this->_Stack)) {
281:             case 2:
282:                 // </SyncHdr> end of header
283:                 $this->handleHeader($this->_currentCommand);
284:                 if ($this->debug) {
285:                     var_dump($this->_currentCommand);
286:                 }
287:                 unset($this->_currentCommand);
288:                 break;
289:             default:
290:                 // pass on to command handler:
291:                 $this->_currentCommand->endElement($uri, $element);
292:                 break;
293:             }
294:         } else {
295:             switch (count($this->_Stack)) {
296:             case 2:
297:                 // </SyncBody> end of SyncBody. Finish everything:
298:                 $this->handleEnd();
299:                 break;
300:             case 3:
301:                 // </[Command]></SyncBody></SyncML>
302:                 // Command finished. Complete parsing and pass on to Handler
303:                 $this->_currentCommand->endElement($uri, $element);
304:                 $this->handleCommand($this->_currentCommand);
305:                 if ($this->debug) {
306:                     var_dump($this->_currentCommand);
307:                 }
308:                 unset($this->_currentCommand);
309:                 break;
310:             default:
311:                 // </...></[Command]></SyncBody></SyncML>
312:                 // pass on to command handler:
313:                 $this->_currentCommand->endElement($uri, $element);
314:                 break;
315:             }
316:         }
317: 
318:         if (isset($this->_chars)) {
319:             unset($this->_chars);
320:         }
321: 
322:         array_pop($this->_Stack);
323:     }
324: 
325:     /**
326:      * Callback public function called by WBXML parser.
327:      */
328:     public function characters($str)
329:     {
330:         if (isset($this->_currentCommand)) {
331:             $this->_currentCommand->characters($str);
332:         } else {
333:             if (isset($this->_chars)) {
334:                 $this->_chars = $this->_chars . $str;
335:             } else {
336:                 $this->_chars = $str;
337:             }
338:         }
339:     }
340: 
341:     /*
342:      * PROCESSING FUNCTIONS
343:      *
344:      * The following functions are called by the callback functions
345:      * and do the actual processing.
346:      */
347: 
348:     /**
349:      * Handles the header logic.
350:      *
351:      * Invoked after header is parsed.
352:      */
353:     public function handleHeader(&$hdr)
354:     {
355:         if (is_object($this->_wbxmlparser)) {
356:             /* The WBXML parser only knows about the charset once parsing is
357:              * started. So setup charset now. */
358:             $this->_xmlWriter->output->setVersion($this->_wbxmlparser->getVersion());
359:             $this->_xmlWriter->output->setCharset($this->_wbxmlparser->getCharsetStr());
360:             $GLOBALS['backend']->setCharset($this->_wbxmlparser->getCharsetStr());
361:         }
362: 
363:         /* Start the session. */
364:         $hdr->setupState();
365:         $state = $GLOBALS['backend']->state;
366:         $state->wbxml = $this->_xmlWriter->isWBXML();
367: 
368:         /* Check auth. */
369:         if (!$state->authenticated) {
370:             $auth = $GLOBALS['backend']->checkAuthentication(
371:                 $hdr->user, $hdr->credData, $hdr->credFormat, $hdr->credType);
372:             if ($auth !== false) {
373:                 $state->authenticated = true;
374:                 $statuscode = Horde_SyncMl::RESPONSE_AUTHENTICATION_ACCEPTED;
375:                 $state->user = $auth;
376:                 $GLOBALS['backend']->setUser($auth);
377:             } else {
378:                 if (!$hdr->credData) {
379:                     $statuscode = Horde_SyncMl::RESPONSE_CREDENTIALS_MISSING;
380:                 } else {
381:                     $statuscode = Horde_SyncMl::RESPONSE_INVALID_CREDENTIALS;
382:                 }
383:                 $GLOBALS['backend']->logMessage('Invalid authentication', 'DEBUG');
384:             }
385:         } else {
386:             $statuscode = Horde_SyncMl::RESPONSE_OK;
387:             $GLOBALS['backend']->setUser($state->user);
388:         }
389: 
390:         /* Create <SyncML>. */
391:         $this->_xmlWriter->outputInit();
392: 
393:         /* Got the state; now write our SyncHdr header. */
394:         $this->_xmlWriter->outputHeader($this->_respURI);
395: 
396:         /* Creates <SyncBody>. */
397:         $this->_xmlWriter->outputBodyStart();
398: 
399:         /* Output status for SyncHdr. */
400:         $this->_xmlWriter->outputStatus('0', 'SyncHdr', $statuscode,
401:                                         $state->targetURI,
402:                                         $state->sourceURI);
403: 
404:         /* Debug logging string. */
405:         $str = 'Authenticated: ' . ($state->authenticated ? 'yes' : 'no')
406:             . '; version: ' . $state->getVerDTD()
407:             . '; message ID: ' . $state->messageID
408:             . '; source URI: ' . $state->sourceURI
409:             . '; target URI: ' . $state->targetURI
410:             . '; user: ' . $state->user
411:             . '; charset: ' . $GLOBALS['backend']->getCharset()
412:             . '; wbxml: ' . ($state->wbxml ? 'yes' : 'no');
413: 
414:         $GLOBALS['backend']->logMessage($str, 'DEBUG');
415:     }
416: 
417:     /**
418:      * Processes one command after it has been completely parsed.
419:      *
420:      * Invoked after a command is parsed.
421:      */
422:     public function handleCommand(&$cmd)
423:     {
424:         $name = $cmd->getCommandName();
425:         if ($name != 'Status' && $name != 'Map' && $name != 'Final' &&
426:             $name != 'Sync' && $name != 'Results') {
427:             /* We've got to do something! This can't be the last packet. */
428:             $GLOBALS['message_expectresponse'] = true;
429:         }
430:         if ($name == 'Final') {
431:             $this->_gotFinal = true;
432:         }
433:         /* Actual processing takes place here. */
434:         $cmd->handleCommand($this->debug);
435:     }
436: 
437:     /**
438:      * Finishes the response.
439:      *
440:      * Invoked after complete message is parsed.
441:      */
442:     public function handleEnd()
443:     {
444:         global $messageFull;
445: 
446:         $state = $GLOBALS['backend']->state;
447: 
448:         /* If there's pending sync data and space left in the message, send
449:          * data now. */
450:         if ($messageFull || $state->hasPendingSyncs()) {
451:             /* still something to do: don't close session. */
452:             $GLOBALS['message_expectresponse'] = true;
453:         }
454: 
455:         if (!$messageFull &&
456:             count($p = $state->getPendingSyncs()) > 0) {
457:             foreach ($p as $pendingSync) {
458:                 if (!$messageFull) {
459:                    $GLOBALS['backend']->logMessage(
460:                        'Continuing sync for syncType ' . $pendingSync, 'DEBUG');
461:                     $sync = &$state->getSync($pendingSync);
462:                     $sync->createSyncOutput($this->_xmlWriter);
463:                 }
464:             }
465:         }
466: 
467:         if (isset($state->curSyncItem)) {
468:             $this->_xmlWriter->outputAlert(
469:                 Horde_SyncMl::ALERT_NO_END_OF_DATA,
470:                 $state->curSyncItem->sync->getClientLocURI(),
471:                 $state->curSyncItem->sync->getServerLocURI(),
472:                 $state->curSyncItem->sync->getServerAnchorLast(),
473:                 $state->curSyncItem->sync->getServerAnchorNext());
474:         }
475: 
476:         /* Don't send the final tag if we haven't sent all sync data yet. */
477:         if ($this->_gotFinal) {
478:             if (!$messageFull &&
479:                 !$state->hasPendingSyncs()) {
480:                 /* Create <Final></Final>. */
481:                 $this->_xmlWriter->outputFinal();
482:                 $GLOBALS['backend']->logMessage('Sending <Final> to client', 'DEBUG');
483:                 $state->delayedFinal = false;
484:             } else {
485:                 $GLOBALS['message_expectresponse'] = true;
486:                 /* Remember to send a Final. */
487:                 $state->delayedFinal = true;
488:             }
489:         } elseif ($state->delayedFinal) {
490:             if (!$messageFull &&
491:                 !$state->hasPendingSyncs()) {
492:                 /* Create <Final></Final>. */
493:                 $this->_xmlWriter->outputFinal();
494:                 $GLOBALS['backend']->logMessage(
495:                     'Sending delayed <Final> to client', 'DEBUG');
496:                 $state->delayedFinal = false;
497:             } else {
498:                 $GLOBALS['message_expectresponse'] = true;
499:             }
500:         }
501: 
502:         /* Create </SyncML>. Message is finished now! */
503:         $this->_xmlWriter->outputEnd();
504: 
505:         if ($this->_gotFinal &&
506:             !$GLOBALS['message_expectresponse'] &&
507:             $state->isAllSyncsComplete()) {
508:             /* This packet did not contain any real actions, just status and
509:              * map. This means we're done. The session can be closed and the
510:              * anchors saved for the next sync. */
511:             foreach ($state->getSyncs() as $sync) {
512:                 $sync->closeSync();
513:             }
514:             $GLOBALS['backend']->logMessage('Session completed and closed', 'DEBUG');
515: 
516:             /* Session can be closed here. */
517:             $GLOBALS['backend']->sessionClose();
518:         } else {
519:             $GLOBALS['backend']->logMessage('Return message completed', 'DEBUG');
520:         }
521:     }
522: }
523: 
API documentation generated by ApiGen