Merge branch 'master' of http://git.syncroton.org/Syncroton
[tine20] / tine20 / library / Syncroton / lib / Syncroton / Model / AEntry.php
1 <?php
2 /**
3  * Syncroton
4  *
5  * @package     Syncroton
6  * @subpackage  Model
7  * @license     http://www.tine20.org/licenses/lgpl.html LGPL Version 3
8  * @copyright   Copyright (c) 2012-2012 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Lars Kneschke <l.kneschke@metaways.de>
10  */
11
12 /**
13  * abstract class to handle ActiveSync entry
14  *
15  * @package     Syncroton
16  * @subpackage  Model
17  */
18
19 abstract class Syncroton_Model_AEntry implements Syncroton_Model_IEntry, IteratorAggregate, Countable
20 {
21     protected $_xmlBaseElement;
22     
23     protected $_elements = array();
24     
25     protected $_properties = array();
26     
27     protected $_dateTimeFormat = "Y-m-d\TH:i:s.000\Z";
28     
29     /**
30      * (non-PHPdoc)
31      * @see Syncroton_Model_IEntry::__construct()
32      */
33     public function __construct($properties = null)
34     {
35         if ($properties instanceof SimpleXMLElement) {
36             $this->setFromSimpleXMLElement($properties);
37         } elseif (is_array($properties)) {
38             $this->setFromArray($properties);
39         }
40     }
41     
42     /**
43      * (non-PHPdoc)
44      * @see Syncroton_Model_IEntry::appendXML()
45      */
46     public function appendXML(DOMElement $domParrent, Syncroton_Model_IDevice $device)
47     {
48         $this->_addXMLNamespaces($domParrent);
49         
50         foreach($this->_elements as $elementName => $value) {
51             // skip empty values
52             if($value === null || $value === '' || (is_array($value) && empty($value))) {
53                 continue;
54             }
55             
56             list ($nameSpace, $elementProperties) = $this->_getElementProperties($elementName);
57             
58             if ($nameSpace == 'Internal') {\r
59                 continue;\r
60             }\r
61             
62             $elementVersion = isset($elementProperties['supportedSince']) ? $elementProperties['supportedSince'] : '12.0';
63             
64             if (version_compare($device->acsversion, $elementVersion, '<')) {
65                 continue;
66             }
67             
68             $nameSpace = 'uri:' . $nameSpace;
69             
70             if (isset($elementProperties['childElement'])) {
71                 $element = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName));
72                 foreach($value as $subValue) {
73                     $subElement = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementProperties['childElement']));
74                     
75                     $this->_appendXMLElement($device, $subElement, $elementProperties, $subValue);
76                     
77                     $element->appendChild($subElement);
78                     
79                 }
80                 $domParrent->appendChild($element);
81             } else {
82                 $element = $domParrent->ownerDocument->createElementNS($nameSpace, ucfirst($elementName));
83                 
84                 $this->_appendXMLElement($device, $element, $elementProperties, $value);
85                 
86                 $domParrent->appendChild($element);
87             }
88             
89         }
90     }
91     
92     /**
93      * (non-PHPdoc)
94      * @see Countable::count()
95      */    \r
96     public function count()
97     {
98         return count($this->_elements);
99     }
100     
101     /**
102      * (non-PHPdoc)
103      * @see IteratorAggregate::getIterator()
104      */
105     public function getIterator() 
106     {\r
107         return new ArrayIterator($this->_elements);\r
108     }
109     
110     /**
111      * (non-PHPdoc)
112      * @see Syncroton_Model_IEntry::getProperties()
113      */
114     public function getProperties()
115     {
116         $properties = array();
117         
118         foreach($this->_properties as $namespace => $namespaceProperties) {
119             $properties = array_merge($properties, array_keys($namespaceProperties));\r
120         }
121         
122         return $properties;
123         
124     }
125     
126     /**
127      * (non-PHPdoc)
128      * @see Syncroton_Model_IEntry::setFromArray()
129      */
130     public function setFromArray(array $properties)
131     {
132         $this->_elements = array();
133         
134         foreach($properties as $key => $value) {
135             try {
136                 $this->$key = $value; //echo __LINE__ . PHP_EOL;
137             } catch (InvalidArgumentException $iae) {
138                 //ignore invalid properties
139                 //echo __LINE__ . PHP_EOL; echo $iae->getMessage(); echo $iae->getTraceAsString();
140             }
141         }
142     }
143     
144     /**\r
145      * set properties from SimpleXMLElement object\r
146      *\r
147      * @param SimpleXMLElement $xmlCollection\r
148      * @throws InvalidArgumentException\r
149      */\r
150     public function setFromSimpleXMLElement(SimpleXMLElement $properties)\r
151     {\r
152         if (!in_array($properties->getName(), (array) $this->_xmlBaseElement)) {
153             throw new InvalidArgumentException('Unexpected element name: ' . $properties->getName());\r
154         }\r
155     \r
156         $this->_elements = array();\r
157     \r
158         foreach (array_keys($this->_properties) as $namespace) {
159             if ($namespace == 'Internal') {
160                 continue;
161             }
162             
163             $this->_parseNamespace($namespace, $properties);\r
164         }\r
165     \r
166         return;\r
167     }
168     
169     /**
170      * add needed xml namespaces to DomDocument
171      * 
172      * @param unknown_type $domParrent
173      */
174     protected function _addXMLNamespaces(DOMElement $domParrent)\r
175     {
176         foreach($this->_properties as $namespace => $namespaceProperties) {
177             // don't add default namespace again
178             if($domParrent->ownerDocument->documentElement->namespaceURI != 'uri:'.$namespace) {
179                 $domParrent->ownerDocument->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:'.$namespace, 'uri:'.$namespace);
180             }\r
181         }
182     }\r
183     
184     protected function _appendXMLElement(Syncroton_Model_IDevice $device, DOMElement $element, $elementProperties, $value)
185     {
186         if ($value instanceof Syncroton_Model_IEntry) {\r
187             $value->appendXML($element, $device);\r
188         } else {\r
189             if ($value instanceof DateTime) {\r
190                 $value = $value->format($this->_dateTimeFormat);
191                 \r
192             } elseif (isset($elementProperties['encoding']) && $elementProperties['encoding'] == 'base64') {\r
193                 if (is_resource($value)) {\r
194                     rewind($value);
195                     $value = stream_get_contents($value);\r
196                 }\r
197                 $value = base64_encode($value);\r
198             }\r
199             \r
200             if ($elementProperties['type'] == 'byteArray') {
201                 $element->setAttributeNS('uri:Syncroton', 'Syncroton:encoding', 'opaque');
202                 // encode to base64; the wbxml encoder will base64_decode it again
203                 // this way we can also transport data, which would break the xmlparser otherwise
204                 $element->appendChild($element->ownerDocument->createCDATASection(base64_encode($value)));
205             } else {
206                 // strip off any non printable control characters
207                 if (!ctype_print($value)) {
208                     $value = $this->_removeControlChars($value);
209                 }
210                 $element->appendChild($element->ownerDocument->createTextNode($value));
211             }\r
212         }\r
213     }
214     
215     /**\r
216      * removed control chars from string which are not allowd in XML values\r
217      *\r
218      * @param  string|array $_dirty\r
219      * @return string\r
220      */\r
221     protected function _removeControlChars($dirty)\r
222     {
223         return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', null, $dirty);\r
224     }\r
225     
226     /**
227      * 
228      * @param unknown_type $element
229      * @throws InvalidArgumentException
230      * @return multitype:unknown
231      */
232     protected function _getElementProperties($element)\r
233     {
234         foreach($this->_properties as $namespace => $namespaceProperties) {
235             if (array_key_exists($element, $namespaceProperties)) {\r
236                 return array($namespace, $namespaceProperties[$element]);\r
237             }\r
238         }\r
239         \r
240         throw new InvalidArgumentException("$element is no valid property of " . get_class($this));\r
241     }
242     
243     protected function _parseNamespace($nameSpace, SimpleXMLElement $properties)
244     {
245         // fetch data from Contacts namespace\r
246         $children = $properties->children("uri:$nameSpace");\r
247         \r
248         foreach ($children as $elementName => $xmlElement) {
249             $elementName = lcfirst($elementName);
250             \r
251             if (!isset($this->_properties[$nameSpace][$elementName])) {
252                 continue;
253             }
254             
255             list (, $elementProperties) = $this->_getElementProperties($elementName);
256             \r
257             switch ($elementProperties['type']) {
258                 case 'container':
259                     if (isset($elementProperties['childElement'])) {
260                         $property = array();
261                         
262                         $childElement = ucfirst($elementProperties['childElement']);
263                         
264                         foreach ($xmlElement->$childElement as $subXmlElement) {
265                             if (isset($elementProperties['class'])) {
266                                 $property[] = new $elementProperties['class']($subXmlElement);
267                             } else {
268                                 $property[] = (string) $subXmlElement;
269                             }
270                         }
271                     } else {
272                         $subClassName = isset($elementProperties['class']) ? $elementProperties['class'] : get_class($this) . ucfirst($elementName);\r
273                         \r
274                         $property = new $subClassName($xmlElement);\r
275                     }
276                     
277                     break;
278                     
279                 case 'datetime':\r
280                     $property = new DateTime((string) $xmlElement, new DateTimeZone('UTC'));\r
281     \r
282                     break;\r
283     \r
284                 case 'number':\r
285                     $property = (int) $xmlElement;\r
286     \r
287                     break;
288                     \r
289                 default:\r
290                     $property = (string) $xmlElement;\r
291     \r
292                     break;\r
293             }
294             \r
295             if (isset($elementProperties['encoding']) && $elementProperties['encoding'] == 'base64') {
296                 $property = base64_decode($property);
297             }
298             
299             $this->$elementName = $property;
300         }\r
301     }
302     \r
303     public function &__get($name)\r
304     {\r
305         $this->_getElementProperties($name);\r
306     \r
307         return $this->_elements[$name];\r
308     }\r
309     \r
310     public function __set($name, $value)\r
311     {\r
312         list ($nameSpace, $properties) = $this->_getElementProperties($name);\r
313         
314         if ($properties['type'] == 'datetime' && !$value instanceof DateTime) {\r
315             throw new InvalidArgumentException("value for $name must be an instance of DateTime");\r
316         }\r
317         \r
318         $this->_elements[$name] = $value;\r
319     }\r
320     
321     public function __isset($name)\r
322     {\r
323         return isset($this->_elements[$name]);\r
324     }\r
325     \r
326     public function __unset($name)\r
327     {\r
328         unset($this->_elements[$name]);\r
329     }
330 }