source: trunk/zpush/backend/vcarddir.php @ 7589

Revision 7589, 24.8 KB checked in by douglas, 11 years ago (diff)

Ticket #3209 - Integrar módulo de sincronização Z-push ao Expresso

Line 
1<?php
2/***********************************************
3* File      :   vcarddir.php
4* Project   :   Z-Push
5* Descr     :   This backend is for vcard directories.
6*
7* Created   :   01.10.2007
8*
9* Copyright 2007 - 2012 Zarafa Deutschland GmbH
10*
11* This program is free software: you can redistribute it and/or modify
12* it under the terms of the GNU Affero General Public License, version 3,
13* as published by the Free Software Foundation with the following additional
14* term according to sec. 7:
15*
16* According to sec. 7 of the GNU Affero General Public License, version 3,
17* the terms of the AGPL are supplemented with the following terms:
18*
19* "Zarafa" is a registered trademark of Zarafa B.V.
20* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
21* The licensing of the Program under the AGPL does not imply a trademark license.
22* Therefore any rights, title and interest in our trademarks remain entirely with us.
23*
24* However, if you propagate an unmodified version of the Program you are
25* allowed to use the term "Z-Push" to indicate that you distribute the Program.
26* Furthermore you may use our trademarks where it is necessary to indicate
27* the intended purpose of a product or service provided you use it in accordance
28* with honest practices in industrial or commercial matters.
29* If you want to propagate modified versions of the Program under the name "Z-Push",
30* you may only do so if you have a written permission by Zarafa Deutschland GmbH
31* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
32*
33* This program is distributed in the hope that it will be useful,
34* but WITHOUT ANY WARRANTY; without even the implied warranty of
35* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
36* GNU Affero General Public License for more details.
37*
38* You should have received a copy of the GNU Affero General Public License
39* along with this program.  If not, see <http://www.gnu.org/licenses/>.
40*
41* Consult LICENSE file for details
42************************************************/
43include_once('lib/default/diffbackend/diffbackend.php');
44
45class BackendVCardDir extends BackendDiff {
46    /**----------------------------------------------------------------------------------------------------------
47     * default backend methods
48     */
49
50    /**
51     * Authenticates the user - NOT EFFECTIVELY IMPLEMENTED
52     * Normally some kind of password check would be done here.
53     * Alternatively, the password could be ignored and an Apache
54     * authentication via mod_auth_* could be done
55     *
56     * @param string        $username
57     * @param string        $domain
58     * @param string        $password
59     *
60     * @access public
61     * @return boolean
62     */
63    public function Logon($username, $domain, $password) {
64        return true;
65    }
66
67    /**
68     * Logs off
69     *
70     * @access public
71     * @return boolean
72     */
73    public function Logoff() {
74        return true;
75    }
76
77    /**
78     * Sends an e-mail
79     * Not implemented here
80     *
81     * @param SyncSendMail  $sm     SyncSendMail object
82     *
83     * @access public
84     * @return boolean
85     * @throws StatusException
86     */
87    public function SendMail($sm) {
88        return false;
89    }
90
91    /**
92     * Returns the waste basket
93     *
94     * @access public
95     * @return string
96     */
97    public function GetWasteBasket() {
98        return false;
99    }
100
101    /**
102     * Returns the content of the named attachment as stream
103     * not implemented
104     *
105     * @param string        $attname
106     *
107     * @access public
108     * @return SyncItemOperationsAttachment
109     * @throws StatusException
110     */
111    public function GetAttachmentData($attname) {
112        return false;
113    }
114
115    /**----------------------------------------------------------------------------------------------------------
116     * implemented DiffBackend methods
117     */
118
119    /**
120     * Returns a list (array) of folders.
121     * In simple implementations like this one, probably just one folder is returned.
122     *
123     * @access public
124     * @return array
125     */
126    public function GetFolderList() {
127        ZLog::Write(LOGLEVEL_DEBUG, 'VCDir::GetFolderList()');
128        $contacts = array();
129        $folder = $this->StatFolder("root");
130        $contacts[] = $folder;
131
132        return $contacts;
133    }
134
135    /**
136     * Returns an actual SyncFolder object
137     *
138     * @param string        $id           id of the folder
139     *
140     * @access public
141     * @return object       SyncFolder with information
142     */
143    public function GetFolder($id) {
144        ZLog::Write(LOGLEVEL_DEBUG, 'VCDir::GetFolder('.$id.')');
145        if($id == "root") {
146            $folder = new SyncFolder();
147            $folder->serverid = $id;
148            $folder->parentid = "0";
149            $folder->displayname = "Contacts";
150            $folder->type = SYNC_FOLDER_TYPE_CONTACT;
151
152            return $folder;
153        } else return false;
154    }
155
156    /**
157     * Returns folder stats. An associative array with properties is expected.
158     *
159     * @param string        $id             id of the folder
160     *
161     * @access public
162     * @return array
163     */
164    public function StatFolder($id) {
165        ZLog::Write(LOGLEVEL_DEBUG, 'VCDir::StatFolder('.$id.')');
166        $folder = $this->GetFolder($id);
167
168        $stat = array();
169        $stat["id"] = $id;
170        $stat["parent"] = $folder->parentid;
171        $stat["mod"] = $folder->displayname;
172
173        return $stat;
174    }
175
176    /**
177     * Creates or modifies a folder
178     * not implemented
179     *
180     * @param string        $folderid       id of the parent folder
181     * @param string        $oldid          if empty -> new folder created, else folder is to be renamed
182     * @param string        $displayname    new folder name (to be created, or to be renamed to)
183     * @param int           $type           folder type
184     *
185     * @access public
186     * @return boolean                      status
187     * @throws StatusException              could throw specific SYNC_FSSTATUS_* exceptions
188     *
189     */
190    public function ChangeFolder($folderid, $oldid, $displayname, $type){
191        return false;
192    }
193
194    /**
195     * Deletes a folder
196     *
197     * @param string        $id
198     * @param string        $parent         is normally false
199     *
200     * @access public
201     * @return boolean                      status - false if e.g. does not exist
202     * @throws StatusException              could throw specific SYNC_FSSTATUS_* exceptions
203     *
204     */
205    public function DeleteFolder($id, $parentid){
206        return false;
207    }
208
209    /**
210     * Returns a list (array) of messages
211     *
212     * @param string        $folderid       id of the parent folder
213     * @param long          $cutoffdate     timestamp in the past from which on messages should be returned
214     *
215     * @access public
216     * @return array/false  array with messages or false if folder is not available
217     */
218    public function GetMessageList($folderid, $cutoffdate) {
219        ZLog::Write(LOGLEVEL_DEBUG, 'VCDir::GetMessageList('.$folderid.')');
220        $messages = array();
221
222        $dir = opendir($this->getPath());
223        if(!$dir)
224            return false;
225
226        while($entry = readdir($dir)) {
227            if(is_dir($this->getPath() .'/'.$entry))
228                continue;
229
230            $message = array();
231            $message["id"] = $entry;
232            $stat = stat($this->getPath() .'/'.$entry);
233            $message["mod"] = $stat["mtime"];
234            $message["flags"] = 1; // always 'read'
235
236            $messages[] = $message;
237        }
238
239        return $messages;
240    }
241
242    /**
243     * Returns the actual SyncXXX object type.
244     *
245     * @param string            $folderid           id of the parent folder
246     * @param string            $id                 id of the message
247     * @param ContentParameters $contentparameters  parameters of the requested message (truncation, mimesupport etc)
248     *
249     * @access public
250     * @return object/false     false if the message could not be retrieved
251     */
252    public function GetMessage($folderid, $id, $contentparameters) {
253        ZLog::Write(LOGLEVEL_DEBUG, 'VCDir::GetMessage('.$folderid.', '.$id.', ..)');
254        if($folderid != "root")
255            return;
256
257        $types = array ('dom' => 'type', 'intl' => 'type', 'postal' => 'type', 'parcel' => 'type', 'home' => 'type', 'work' => 'type',
258            'pref' => 'type', 'voice' => 'type', 'fax' => 'type', 'msg' => 'type', 'cell' => 'type', 'pager' => 'type',
259            'bbs' => 'type', 'modem' => 'type', 'car' => 'type', 'isdn' => 'type', 'video' => 'type',
260            'aol' => 'type', 'applelink' => 'type', 'attmail' => 'type', 'cis' => 'type', 'eworld' => 'type',
261            'internet' => 'type', 'ibmmail' => 'type', 'mcimail' => 'type',
262            'powershare' => 'type', 'prodigy' => 'type', 'tlx' => 'type', 'x400' => 'type',
263            'gif' => 'type', 'cgm' => 'type', 'wmf' => 'type', 'bmp' => 'type', 'met' => 'type', 'pmb' => 'type', 'dib' => 'type',
264            'pict' => 'type', 'tiff' => 'type', 'pdf' => 'type', 'ps' => 'type', 'jpeg' => 'type', 'qtime' => 'type',
265            'mpeg' => 'type', 'mpeg2' => 'type', 'avi' => 'type',
266            'wave' => 'type', 'aiff' => 'type', 'pcm' => 'type',
267            'x509' => 'type', 'pgp' => 'type', 'text' => 'value', 'inline' => 'value', 'url' => 'value', 'cid' => 'value', 'content-id' => 'value',
268            '7bit' => 'encoding', '8bit' => 'encoding', 'quoted-printable' => 'encoding', 'base64' => 'encoding',
269        );
270
271
272        // Parse the vcard
273        $message = new SyncContact();
274
275        $data = file_get_contents($this->getPath() . "/" . $id);
276        $data = str_replace("\x00", '', $data);
277        $data = str_replace("\r\n", "\n", $data);
278        $data = str_replace("\r", "\n", $data);
279        $data = preg_replace('/(\n)([ \t])/i', '', $data);
280
281        $lines = explode("\n", $data);
282
283        $vcard = array();
284        foreach($lines as $line) {
285            if (trim($line) == '')
286                continue;
287            $pos = strpos($line, ':');
288            if ($pos === false)
289                continue;
290
291            $field = trim(substr($line, 0, $pos));
292            $value = trim(substr($line, $pos+1));
293
294            $fieldparts = preg_split('/(?<!\\\\)(\;)/i', $field, -1, PREG_SPLIT_NO_EMPTY);
295
296            $type = strtolower(array_shift($fieldparts));
297
298            $fieldvalue = array();
299
300            foreach ($fieldparts as $fieldpart) {
301                if(preg_match('/([^=]+)=(.+)/', $fieldpart, $matches)){
302                    if(!in_array(strtolower($matches[1]),array('value','type','encoding','language')))
303                        continue;
304                    if(isset($fieldvalue[strtolower($matches[1])]) && is_array($fieldvalue[strtolower($matches[1])])){
305                        $fieldvalue[strtolower($matches[1])] = array_merge($fieldvalue[strtolower($matches[1])], preg_split('/(?<!\\\\)(\,)/i', $matches[2], -1, PREG_SPLIT_NO_EMPTY));
306                    }else{
307                        $fieldvalue[strtolower($matches[1])] = preg_split('/(?<!\\\\)(\,)/i', $matches[2], -1, PREG_SPLIT_NO_EMPTY);
308                    }
309                }else{
310                    if(!isset($types[strtolower($fieldpart)]))
311                        continue;
312                    $fieldvalue[$types[strtolower($fieldpart)]][] = $fieldpart;
313                }
314            }
315            //
316            switch ($type) {
317                case 'categories':
318                    //case 'nickname':
319                    $val = preg_split('/(?<!\\\\)(\,)/i', $value);
320                    $val = array_map("w2ui", $val);
321                    break;
322                default:
323                    $val = preg_split('/(?<!\\\\)(\;)/i', $value);
324                    break;
325            }
326            if(isset($fieldvalue['encoding'][0])){
327                switch(strtolower($fieldvalue['encoding'][0])){
328                    case 'q':
329                    case 'quoted-printable':
330                        foreach($val as $i => $v){
331                            $val[$i] = quoted_printable_decode($v);
332                        }
333                        break;
334                    case 'b':
335                    case 'base64':
336                        foreach($val as $i => $v){
337                            $val[$i] = base64_decode($v);
338                        }
339                        break;
340                }
341            }else{
342                foreach($val as $i => $v){
343                    $val[$i] = $this->unescape($v);
344                }
345            }
346            $fieldvalue['val'] = $val;
347            $vcard[$type][] = $fieldvalue;
348        }
349
350        if(isset($vcard['email'][0]['val'][0]))
351            $message->email1address = $vcard['email'][0]['val'][0];
352        if(isset($vcard['email'][1]['val'][0]))
353            $message->email2address = $vcard['email'][1]['val'][0];
354        if(isset($vcard['email'][2]['val'][0]))
355            $message->email3address = $vcard['email'][2]['val'][0];
356
357        if(isset($vcard['tel'])){
358            foreach($vcard['tel'] as $tel) {
359                if(!isset($tel['type'])){
360                    $tel['type'] = array();
361                }
362                if(in_array('car', $tel['type'])){
363                    $message->carphonenumber = $tel['val'][0];
364                }elseif(in_array('pager', $tel['type'])){
365                    $message->pagernumber = $tel['val'][0];
366                }elseif(in_array('cell', $tel['type'])){
367                    $message->mobilephonenumber = $tel['val'][0];
368                }elseif(in_array('home', $tel['type'])){
369                    if(in_array('fax', $tel['type'])){
370                        $message->homefaxnumber = $tel['val'][0];
371                    }elseif(empty($message->homephonenumber)){
372                        $message->homephonenumber = $tel['val'][0];
373                    }else{
374                        $message->home2phonenumber = $tel['val'][0];
375                    }
376                }elseif(in_array('work', $tel['type'])){
377                    if(in_array('fax', $tel['type'])){
378                        $message->businessfaxnumber = $tel['val'][0];
379                    }elseif(empty($message->businessphonenumber)){
380                        $message->businessphonenumber = $tel['val'][0];
381                    }else{
382                        $message->business2phonenumber = $tel['val'][0];
383                    }
384                }elseif(empty($message->homephonenumber)){
385                    $message->homephonenumber = $tel['val'][0];
386                }elseif(empty($message->home2phonenumber)){
387                    $message->home2phonenumber = $tel['val'][0];
388                }else{
389                    $message->radiophonenumber = $tel['val'][0];
390                }
391            }
392        }
393        //;;street;city;state;postalcode;country
394        if(isset($vcard['adr'])){
395            foreach($vcard['adr'] as $adr) {
396                if(empty($adr['type'])){
397                    $a = 'other';
398                }elseif(in_array('home', $adr['type'])){
399                    $a = 'home';
400                }elseif(in_array('work', $adr['type'])){
401                    $a = 'business';
402                }else{
403                    $a = 'other';
404                }
405                if(!empty($adr['val'][2])){
406                    $b=$a.'street';
407                    $message->$b = w2ui($adr['val'][2]);
408                }
409                if(!empty($adr['val'][3])){
410                    $b=$a.'city';
411                    $message->$b = w2ui($adr['val'][3]);
412                }
413                if(!empty($adr['val'][4])){
414                    $b=$a.'state';
415                    $message->$b = w2ui($adr['val'][4]);
416                }
417                if(!empty($adr['val'][5])){
418                    $b=$a.'postalcode';
419                    $message->$b = w2ui($adr['val'][5]);
420                }
421                if(!empty($adr['val'][6])){
422                    $b=$a.'country';
423                    $message->$b = w2ui($adr['val'][6]);
424                }
425            }
426        }
427
428        if(!empty($vcard['fn'][0]['val'][0]))
429            $message->fileas = w2ui($vcard['fn'][0]['val'][0]);
430        if(!empty($vcard['n'][0]['val'][0]))
431            $message->lastname = w2ui($vcard['n'][0]['val'][0]);
432        if(!empty($vcard['n'][0]['val'][1]))
433            $message->firstname = w2ui($vcard['n'][0]['val'][1]);
434        if(!empty($vcard['n'][0]['val'][2]))
435            $message->middlename = w2ui($vcard['n'][0]['val'][2]);
436        if(!empty($vcard['n'][0]['val'][3]))
437            $message->title = w2ui($vcard['n'][0]['val'][3]);
438        if(!empty($vcard['n'][0]['val'][4]))
439            $message->suffix = w2ui($vcard['n'][0]['val'][4]);
440        if(!empty($vcard['bday'][0]['val'][0])){
441            $tz = date_default_timezone_get();
442            date_default_timezone_set('UTC');
443            $message->birthday = strtotime($vcard['bday'][0]['val'][0]);
444            date_default_timezone_set($tz);
445        }
446        if(!empty($vcard['org'][0]['val'][0]))
447            $message->companyname = w2ui($vcard['org'][0]['val'][0]);
448        if(!empty($vcard['note'][0]['val'][0])){
449            $message->body = w2ui($vcard['note'][0]['val'][0]);
450            $message->bodysize = strlen($vcard['note'][0]['val'][0]);
451            $message->bodytruncated = 0;
452        }
453        if(!empty($vcard['role'][0]['val'][0]))
454            $message->jobtitle = w2ui($vcard['role'][0]['val'][0]);//$vcard['title'][0]['val'][0]
455        if(!empty($vcard['url'][0]['val'][0]))
456            $message->webpage = w2ui($vcard['url'][0]['val'][0]);
457        if(!empty($vcard['categories'][0]['val']))
458            $message->categories = $vcard['categories'][0]['val'];
459
460        if(!empty($vcard['photo'][0]['val'][0]))
461            $message->picture = base64_encode($vcard['photo'][0]['val'][0]);
462
463        return $message;
464    }
465
466    /**
467     * Returns message stats, analogous to the folder stats from StatFolder().
468     *
469     * @param string        $folderid       id of the folder
470     * @param string        $id             id of the message
471     *
472     * @access public
473     * @return array
474     */
475    public function StatMessage($folderid, $id) {
476        ZLog::Write(LOGLEVEL_DEBUG, 'VCDir::StatMessage('.$folderid.', '.$id.')');
477        if($folderid != "root")
478            return false;
479
480        $stat = stat($this->getPath() . "/" . $id);
481
482        $message = array();
483        $message["mod"] = $stat["mtime"];
484        $message["id"] = $id;
485        $message["flags"] = 1;
486
487        return $message;
488    }
489
490    /**
491     * Called when a message has been changed on the mobile.
492     * This functionality is not available for emails.
493     *
494     * @param string        $folderid       id of the folder
495     * @param string        $id             id of the message
496     * @param SyncXXX       $message        the SyncObject containing a message
497     *
498     * @access public
499     * @return array                        same return value as StatMessage()
500     * @throws StatusException              could throw specific SYNC_STATUS_* exceptions
501     */
502    public function ChangeMessage($folderid, $id, $message) {
503        ZLog::Write(LOGLEVEL_DEBUG, 'VCDir::ChangeMessage('.$folderid.', '.$id.', ..)');
504        $mapping = array(
505            'fileas' => 'FN',
506            'lastname;firstname;middlename;title;suffix' => 'N',
507            'email1address' => 'EMAIL;INTERNET',
508            'email2address' => 'EMAIL;INTERNET',
509            'email3address' => 'EMAIL;INTERNET',
510            'businessphonenumber' => 'TEL;WORK',
511            'business2phonenumber' => 'TEL;WORK',
512            'businessfaxnumber' => 'TEL;WORK;FAX',
513            'homephonenumber' => 'TEL;HOME',
514            'home2phonenumber' => 'TEL;HOME',
515            'homefaxnumber' => 'TEL;HOME;FAX',
516            'mobilephonenumber' => 'TEL;CELL',
517            'carphonenumber' => 'TEL;CAR',
518            'pagernumber' => 'TEL;PAGER',
519            ';;businessstreet;businesscity;businessstate;businesspostalcode;businesscountry' => 'ADR;WORK',
520            ';;homestreet;homecity;homestate;homepostalcode;homecountry' => 'ADR;HOME',
521            ';;otherstreet;othercity;otherstate;otherpostalcode;othercountry' => 'ADR',
522            'companyname' => 'ORG',
523            'body' => 'NOTE',
524            'jobtitle' => 'ROLE',
525            'webpage' => 'URL',
526        );
527        $data = "BEGIN:VCARD\nVERSION:2.1\nPRODID:Z-Push\n";
528        foreach($mapping as $k => $v){
529            $val = '';
530            $ks = explode(';', $k);
531            foreach($ks as $i){
532                if(!empty($message->$i))
533                    $val .= $this->escape($message->$i);
534                $val.=';';
535            }
536            if(empty($val))
537                continue;
538            $val = substr($val,0,-1);
539            if(strlen($val)>50){
540                $data .= $v.":\n\t".substr(chunk_split($val, 50, "\n\t"), 0, -1);
541            }else{
542                $data .= $v.':'.$val."\n";
543            }
544        }
545        if(!empty($message->categories))
546            $data .= 'CATEGORIES:'.implode(',', $this->escape($message->categories))."\n";
547        if(!empty($message->picture))
548            $data .= 'PHOTO;ENCODING=BASE64;TYPE=JPEG:'."\n\t".substr(chunk_split($message->picture, 50, "\n\t"), 0, -1);
549        if(isset($message->birthday))
550            $data .= 'BDAY:'.date('Y-m-d', $message->birthday)."\n";
551        $data .= "END:VCARD";
552
553// not supported: anniversary, assistantname, assistnamephonenumber, children, department, officelocation, radiophonenumber, spouse, rtf
554
555        if(!$id){
556            if(!empty($message->fileas)){
557                $name = u2wi($message->fileas);
558            }elseif(!empty($message->lastname)){
559                $name = $name = u2wi($message->lastname);
560            }elseif(!empty($message->firstname)){
561                $name = $name = u2wi($message->firstname);
562            }elseif(!empty($message->companyname)){
563                $name = $name = u2wi($message->companyname);
564            }else{
565                $name = 'unknown';
566            }
567            $name = preg_replace('/[^a-z0-9 _-]/i', '', $name);
568            $id = $name.'.vcf';
569            $i = 0;
570            while(file_exists($this->getPath().'/'.$id)){
571                $i++;
572                $id = $name.$i.'.vcf';
573            }
574        }
575        file_put_contents($this->getPath().'/'.$id, $data);
576        return $this->StatMessage($folderid, $id);
577    }
578
579    /**
580     * Changes the 'read' flag of a message on disk
581     *
582     * @param string        $folderid       id of the folder
583     * @param string        $id             id of the message
584     * @param int           $flags          read flag of the message
585     *
586     * @access public
587     * @return boolean                      status of the operation
588     * @throws StatusException              could throw specific SYNC_STATUS_* exceptions
589     */
590    public function SetReadFlag($folderid, $id, $flags) {
591        return false;
592    }
593
594    /**
595     * Called when the user has requested to delete (really delete) a message
596     *
597     * @param string        $folderid       id of the folder
598     * @param string        $id             id of the message
599     *
600     * @access public
601     * @return boolean                      status of the operation
602     * @throws StatusException              could throw specific SYNC_STATUS_* exceptions
603     */
604    public function DeleteMessage($folderid, $id) {
605        return unlink($this->getPath() . '/' . $id);
606    }
607
608    /**
609     * Called when the user moves an item on the PDA from one folder to another
610     * not implemented
611     *
612     * @param string        $folderid       id of the source folder
613     * @param string        $id             id of the message
614     * @param string        $newfolderid    id of the destination folder
615     *
616     * @access public
617     * @return boolean                      status of the operation
618     * @throws StatusException              could throw specific SYNC_MOVEITEMSSTATUS_* exceptions
619     */
620    public function MoveMessage($folderid, $id, $newfolderid) {
621        return false;
622    }
623
624
625    /**----------------------------------------------------------------------------------------------------------
626     * private vcard-specific internals
627     */
628
629    /**
630     * The path we're working on
631     *
632     * @access private
633     * @return string
634     */
635    private function getPath() {
636        return str_replace('%u', $this->store, VCARDDIR_DIR);
637    }
638
639    /**
640     * Escapes a string
641     *
642     * @param string        $data           string to be escaped
643     *
644     * @access private
645     * @return string
646     */
647    function escape($data){
648        if (is_array($data)) {
649            foreach ($data as $key => $val) {
650                $data[$key] = $this->escape($val);
651            }
652            return $data;
653        }
654        $data = str_replace("\r\n", "\n", $data);
655        $data = str_replace("\r", "\n", $data);
656        $data = str_replace(array('\\', ';', ',', "\n"), array('\\\\', '\\;', '\\,', '\\n'), $data);
657        return u2wi($data);
658    }
659
660    /**
661     * Un-escapes a string
662     *
663     * @param string        $data           string to be un-escaped
664     *
665     * @access private
666     * @return string
667     */
668    function unescape($data){
669        $data = str_replace(array('\\\\', '\\;', '\\,', '\\n','\\N'),array('\\', ';', ',', "\n", "\n"),$data);
670        return $data;
671    }
672};
673?>
Note: See TracBrowser for help on using the repository browser.