0012180: fix broken xml input (#2)
[tine20] / tine20 / Tinebase / Import / CalDav / Client.php
1 <?php
2
3 /**
4  * Tine 2.0
5  * 
6  * @package     Tinebase
7  * @subpackage  Import
8  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
9  * @author      Paul Mehrer <p.mehrer@metaways.de>
10  * @copyright   Copyright (c) 2014 Metaways Infosystems GmbH (http://www.metaways.de)
11  */
12
13 /**
14  * Tinebase_Import_CalDav
15  * 
16  * @package     Tinebase
17  * @subpackage  Import
18  * 
19  */
20 class Tinebase_Import_CalDav_Client extends \Sabre\DAV\Client
21 {
22     /**
23      * used to overwrite default retry behavior (if != null)
24      * 
25      * @var integer
26      */
27     protected $_requestTries = null;
28     
29     protected $currentUserPrincipal = '';
30     protected $calendarHomeSet = '';
31     protected $principals = array();
32     protected $principalGroups = array();
33     
34     protected $requestLogFH;
35     
36     const findCurrentUserPrincipalRequest = 
37 '<?xml version="1.0"?>
38 <d:propfind xmlns:d="DAV:">
39   <d:prop>
40     <d:current-user-principal />
41   </d:prop>
42 </d:propfind>';
43
44     const findCalendarHomeSetRequest =
45 '<?xml version="1.0"?>
46 <d:propfind xmlns:d="DAV:">
47   <d:prop>
48     <x:calendar-home-set xmlns:x="urn:ietf:params:xml:ns:caldav"/>
49   </d:prop>
50 </d:propfind>';
51     
52     const resolvePrincipalRequest =
53 '<?xml version="1.0"?>
54 <d:propfind xmlns:d="DAV:">
55   <d:prop>
56     <d:group-member-set />
57     <d:displayname />
58   </d:prop>
59 </d:propfind>';
60     
61     public function __construct(array $a)
62     {
63         parent::__construct($a);
64         
65         //$this->requestLogFH = fopen('/var/log/tine20/requestLog', 'w');
66         
67         $this->propertyMap['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'] = 'Sabre\CalDAV\Property\SupportedCalendarComponentSet';
68         $this->propertyMap['{DAV:}acl'] = 'Sabre\DAVACL\Property\Acl';
69         $this->propertyMap['{DAV:}group-member-set'] = 'Tinebase_Import_CalDav_GroupMemberSet';
70     }
71     
72     /**
73      * findCurrentUserPrincipal
74      * - result ($this->currentUserPrincipal) is cached for 1 week
75      * 
76      * @param number $tries
77      * @return boolean
78      */
79     public function findCurrentUserPrincipal($tries = 1)
80     {
81         $cacheId = Tinebase_Helper::convertCacheId('findCurrentUserPrincipal' . $this->userName);
82         if (Tinebase_Core::getCache()->test($cacheId)) {
83             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . ' ' . __LINE__
84                     . ' Loading user principal from cache');
85             
86             $this->currentUserPrincipal = Tinebase_Core::getCache()->load($cacheId);
87             $user = $this->_setUser();
88             if (! $user) {
89                 return false;
90             }
91             
92             return true;
93         }
94         
95         $result = $this->calDavRequest('PROPFIND', '/principals/', self::findCurrentUserPrincipalRequest, 0, $tries);
96         if (isset($result['{DAV:}current-user-principal']))
97         {
98             $this->currentUserPrincipal = $result['{DAV:}current-user-principal'];
99             $user = $this->_setUser();
100             if (! $user) {
101                 return false;
102             }
103             
104             Tinebase_Core::getCache()->save($this->currentUserPrincipal, $cacheId, array(), /* 1 week */ 24*3600*7);
105             return true;
106         }
107         
108         Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' couldn\'t find current users principal');
109         return false;
110     }
111     
112     protected function _setUser()
113     {
114         try {
115             $user = Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountLoginName', $this->userName, 'Tinebase_Model_FullUser');
116             Tinebase_Core::set(Tinebase_Core::USER, $user);
117             $credentialCache = Tinebase_Auth_CredentialCache::getInstance()->cacheCredentials($this->userName, $this->password);
118             Tinebase_Core::set(Tinebase_Core::USERCREDENTIALCACHE, $credentialCache);
119         } catch (Tinebase_Exception_NotFound $e) {
120             Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Can\'t find tine20 user: ' . $this->userName);
121             return null;
122         }
123         
124         $this->principals[$this->currentUserPrincipal] = $user;
125         
126         return $user;
127     }
128     
129     public function findCurrentUserPrincipalForUsers(array &$users)
130     {
131         foreach ($users as $username => $pwd) {
132             $this->userName = $username;
133             $this->password = $pwd;
134             
135             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . ' ' . __LINE__
136                 . ' Find principal for user ' . $this->userName);
137             try {
138                 if (! $this->findCurrentUserPrincipal()) {
139                     if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . ' ' . __LINE__
140                         . ' Skipping ' . $username);
141                     unset($users[$username]);
142                 }
143             } catch (Tinebase_Exception $te) {
144                 // TODO should use better exception (Not_Authenticated, ...)
145                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . ' ' . __LINE__
146                         . ' Skipping ' . $username);
147                 unset($users[$username]);
148             }
149         }
150         return count($users) > 0;
151     }
152     
153     /**
154      * findCalendarHomeSet
155      * - result ($this->calendarHomeSet) is cached for 1 week
156      * 
157      * @return boolean
158      */
159     public function findCalendarHomeSet()
160     {
161         if ('' == $this->currentUserPrincipal && ! $this->findCurrentUserPrincipal(/* tries = */ 3)) {
162             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . ' ' . __LINE__
163                     . ' No principal found for user ' . $this->userName);
164             return false;
165         }
166         $cacheId = Tinebase_Helper::convertCacheId('findCalendarHomeSet' . $this->userName);
167         if (Tinebase_Core::getCache()->test($cacheId)) {
168             $this->calendarHomeSet = Tinebase_Core::getCache()->load($cacheId);
169             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . ' ' . __LINE__
170                     . ' Loading user home set from cache');
171             return true;
172         }
173         
174         $result = $this->calDavRequest('PROPFIND', $this->currentUserPrincipal, self::findCalendarHomeSetRequest);
175         
176         if (isset($result['{urn:ietf:params:xml:ns:caldav}calendar-home-set'])) {
177             $this->calendarHomeSet = $result['{urn:ietf:params:xml:ns:caldav}calendar-home-set'];
178             Tinebase_Core::getCache()->save($this->calendarHomeSet, $cacheId, array(), /* 1 week */ 24*3600*7);
179             return true;
180         }
181         
182         Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' couldn\'t find calendar homeset');
183         return false;
184     }
185     
186     /**
187      * resolve principals
188      * 
189      * @param array $privileges
190      */
191     public function resolvePrincipals(array $privileges)
192     {
193         foreach ($privileges as $ace)
194         {
195             if ( $ace['principal'] == '{DAV:}authenticated' || $ace['principal'] == $this->currentUserPrincipal ||
196                  isset($this->principals[$ace['principal']]) || isset($this->principalGroups[$ace['principal']])) {
197                      continue;
198             }
199             
200             $result = $this->calDavRequest('PROPFIND', $ace['principal'], self::resolvePrincipalRequest);
201             if (isset($result['{DAV:}group-member-set'])) {
202                 $principals = $result['{DAV:}group-member-set']->getPrincipals();
203                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . ' ' . __LINE__
204                         . ' ' . print_r($principals, true));
205                 $this->principalGroups[$ace['principal']] = $result['{DAV:}group-member-set']->getPrincipals();
206             }
207         }
208     }
209     
210     public function clearCurrentUserData()
211     {
212         $this->currentUserPrincipal = '';
213         $this->calendarHomeSet = '';
214     }
215     
216     /**
217      * perform calDavRequest
218      * 
219      * @param string $method
220      * @param string $uri
221      * @param strubg $body
222      * @param number $depth
223      * @param number $tries
224      * @param number $sleep
225      * @throws Tinebase_Exception
226      */
227     public function calDavRequest($method, $uri, $body, $depth = 0, $tries = 10, $sleep = 30)
228     {
229         $response = null;
230         if ($this->_requestTries !== null) {
231             // overwrite default retry behavior
232             $tries = $this->_requestTries;
233         }
234         while ($tries > 0)
235         {
236             try {
237                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
238                         . ' Sending ' . $method . ' request for uri ' . $uri . ' ...');
239                 $response = $this->request($method, $uri, $body, array(
240                     'Depth' => $depth,
241                     'Content-Type' => 'text/xml',
242                 ));
243             } catch (Exception $e) {
244                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
245                     Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
246                             . ' Caldav request failed: '
247                             . '(' . $this->userName . ')' . $method . ' ' . $uri . "\n" . $body
248                             . "\n" . $e->getMessage());
249                 if (--$tries > 0) {
250                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
251                             . ' Sleeping ' . $sleep . ' seconds and retrying ... ');
252                     sleep($sleep);
253                 }
254                 continue;
255             }
256             break;
257         }
258         
259         if (! $response) {
260             throw new Tinebase_Exception("no response");
261         }
262         
263         $result = $this->parseMultiStatus($response['body']);
264         
265         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
266                 . ' Uri: ' . $uri . ' | request: ' . $body . ' | response: ' . print_r($response, true));
267         
268         // If depth was 0, we only return the top item
269         if ($depth===0) {
270             reset($result);
271             $result = current($result);
272             $result = isset($result[200])?$result[200]:array();
273             
274             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
275                     . ' Result (depth 0): ' . var_export($result, true));
276             
277             return $result;
278         }
279         
280         $newResult = array();
281         foreach($result as $href => $statusList)
282         {
283             $newResult[$href] = isset($statusList[200])?$statusList[200]:array();
284         }
285
286         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
287                 . ' Result: ' . var_export($newResult, true));
288         
289         return $newResult;
290     }
291
292     /**
293      * Parses a WebDAV multistatus response body
294      *
295      * @param string $body xml body
296      * @return array
297      */
298     public function parseMultiStatus($body)
299     {
300         $oldSetting = libxml_use_internal_errors(true);
301
302         try {
303             $result = parent::parseMultiStatus($body);
304
305             if (count($xmlErrors = libxml_get_errors()) > 0) {
306                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
307                     . ' XML errors occured: ' . print_r($xmlErrors, true));
308             }
309             libxml_clear_errors();
310             libxml_use_internal_errors($oldSetting);
311
312         } catch(InvalidArgumentException $e) {
313             libxml_clear_errors();
314             libxml_use_internal_errors($oldSetting);
315
316             // remove possible broken chars here to avoid simplexml_load_string errors
317             // this line may throw an Exception again! thats why the libxml_* functions are called in try and catch!
318             $result = parent::parseMultiStatus(Tinebase_Helper::removeIllegalXMLChars($body));
319         }
320
321         return $result;
322     }
323 }