Code

0c07fba09b147517f3dfcef2e06091274c4ca599
[gosa.git] / gosa-core / include / class_gosaSupportDaemon.inc
1 <?php
2 /*
3  * This code is part of GOsa (http://www.gosa-project.org)
4  * Copyright (C) 2003-2008 GONICUS GmbH
5  *
6  * ID: $$Id$$
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, write to the Free Software
20  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21  */
23 class gosaSupportDaemon
24 {
25   private $s_host       = "";
26   private $i_port       = 0;
27   private $s_encryption_key = "";
29   private $o_sock       = NULL;
30   private $f_timeout    = 2;
31   private $s_error      = "";
32   private $b_error      = FALSE;
34   private $is_connected     = FALSE;
37   /*! \brief  Creates a new gosaSupportDaemon object.
38     @param string   Host    The Host where the daemon is running on.  
39     @param integer  Port    The port which the daemon use.
40     @param string   Key     The encryption string.
41     @param boolean  Connect Directly connect to daemon socket.
42     @param float    Timeout The timelimit for all socket actions.
43    */
44   public function __construct($connect=TRUE,$timeout=2)
45   {
46     #FIXME: bad idea about referencing global variables from within classes
47     global $config;
49     # load from config, store statically
50     if (isset($config->current['GOSA_SI'])){
52       if ($this->s_host == ""){
53         $this->s_host= preg_replace("/^.*@([^:]+):.*$/", "$1", $config->current['GOSA_SI']);
54         $this->i_port= preg_replace("/^.*@[^:]+:(.*)$/", "$1", $config->current['GOSA_SI']);
55         $this->s_encryption_key = preg_replace("/^(.*)@[^:]+:.*$/", "$1", $config->current['GOSA_SI']);
56       }
58       $this->f_timeout = $timeout;
59       if($connect){
60         $this->connect();
61       }
62     }
63   }
66   /*! \brief  Establish daemon connection. 
67     @return boolean Returns true if the connection was succesfully established. 
68    */
69   public function connect()
70   {
71     $this->o_sock = new Socket_Client($this->s_host,$this->i_port,TRUE,$this->f_timeout);
72     if($this->o_sock->connected()){ 
73       $this->o_sock->setEncryptionKey($this->s_encryption_key); 
74       $this->is_connected = TRUE;
75     }else{
76       $this->set_error($this->o_sock->get_error());
77       $this->disconnect();
78       new log("debug","gosaSupportDaemon::connect()", "Could not connect to server.", array(),$this->get_error());
79     }
80     return($this->is_connected);
81   }
84   /*! \brief  Disconnect from gosa daemon.
85    */
86   public function disconnect()
87   {
88     $this->o_sock->close();
89     $this->is_connected = FALSE;
90   }
93   /*! \brief  Sets an error message, which can be returned with get_error().
94     @param  string  The Error message,
95    */
96   private function set_error($str)
97   {
98     $this->b_error = TRUE;
99     $this->s_error = $str;
100   }
103   /*! \brief  Sets an error message, which can be returned with get_error().
104     @param  string  The Error message,
105    */
106   private function reset_error()
107   {
108     $this->b_error = FALSE;
109     $this->s_error = "";
110   }
113   /*! \brief  Checks if an error occured.
114     @return boolean returns TRUE or FALSE, whether there is an error or not.
115    */
116   public function is_error()
117   {
118     return($this->b_error);
119   }
122   /*! \brief  Returns the last error. 
123     @return Returns the last error.
124    */
125   public function get_error()
126   {
127     $str = $this->s_error;
128     $str = preg_replace("/ /","&nbsp;",$str);
129     return($str);
130   }
133   /*! \brief  Returns an array containing all queued entries.
134     @return Array All queued entries as an array.
135    */
136   public function get_queued_entries($event_types = array("*"),$from=-1,$to=-1,$sort="timestamp DESC")
137   {
138     $this->reset_error();
139     $ret = array();
141     $tags = "";
142     foreach($event_types as $type){
143       $tags .= "<phrase><headertag>".$type."</headertag></phrase>";
144     }
145     if(count($event_types) > 1){
146       $tags = "<connector>or</connector>".$tags;
147     }
148     if(count($event_types)){
149       $tags = "<where><clause>".$tags."</clause></where>";
150     }
152     $xml_msg = "<xml>
153       <header>gosa_query_jobdb</header>
154       <target>GOSA</target>
155       <source>GOSA</source>
156       ".$tags."
158       <orderby>".$sort."</orderby>";
159 if($from != -1 && $to != -1){
160 $xml_msg.= "
161       <limit>
162        <from>".$from."</from>
163        <to>".$to."</to>
164       </limit>";
166 $xml_msg.= "
167       </xml>";
169     if($this->connect()){
170       $this->o_sock->write($xml_msg);
171       $str = trim($this->o_sock->read());
172       $entries = $this->xml_to_array($str);
173       if(isset($entries['XML']) && is_array($entries['XML'])){
175         /* Check if returned values represent a valid answer */
176         if(isset($entries['XML'])){
177           
178           /* Unset header tags */
179           foreach(array("HEADER","SOURCE","TARGET") as $type){
180             unset($entries['XML'][$type]);
181           }
182           $ret = $entries['XML']; 
183         }
184       }
185     }
186     
187     return($ret);
188   }
191   /*! \brief  Checks if the given ids are used queue ids.
192     @param  Array   The ids we want to check..
193     @return Array   An array containing all ids as index and TRUE/FALSE as value. 
194    */
195   public function ids_exist($ids)
196   {
197     if(!is_array($ids)){
198       trigger_error("Requires an array as parameter.");
199       return;
200     }
201     $this->reset_error();
203     $ret = array();
205     $xml_msg = "<xml>
206       <header>gosa_query_jobdb</header>
207       <target>GOSA</target>
208       <source>GOSA</source>
209       <where>
210       <clause>
211       <connector>or</connector>";
212     foreach($ids as $id){
213       $xml_msg .= "<phrase>
214         <operator>eq</operator>
215         <id>".$id."</id>
216         </phrase>";
217     }
218     $xml_msg .= "</clause>
219       </where>
220       </xml>";
222     if($this->connect()){
223       $this->o_sock->write($xml_msg);
224       $str = trim($this->o_sock->read());
225       $entries = $this->xml_to_array($str);
226       if(isset($entries['XML']) && is_array($entries['XML'])){
227         foreach($entries['XML'] as $entry){
228           if(isset($entry['ID'])){
229             $ret[] = $entry['ID'];
230           }
231         }
232       }
233     }
234     return($ret);
235   }
238   /*! \brief  Returns an entry containing all requested ids.
239     @param  Array   The IDs of the entries we want to return.
240     @return Array   Of the requested entries. 
241    */
242   public function get_entries_by_mac($macs)
243   {
244     if(!is_array($macs)){
245       trigger_error("Requires an array as parameter.");
246       return;
247     }
248     $this->reset_error();
250     $ret = array();
252     $xml_msg = "<xml>
253       <header>gosa_query_jobdb</header>
254       <target>GOSA</target>
255       <source>GOSA</source>
256       <where>
257       <clause>
258       <connector>or</connector>";
259     foreach($macs as $mac){
260       $xml_msg .= "<phrase>
261         <operator>eq</operator>
262         <macaddress>".$mac."</macaddress>
263         </phrase>";
264     }
265     $xml_msg .= "</clause>
266       </where>
267       </xml>";
269     if($this->connect()){
270       $this->o_sock->write($xml_msg);
271       $str = trim($this->o_sock->read());
272       $entries = $this->xml_to_array($str); 
273       if(isset($entries['XML'])){
274         foreach($entries['XML'] as $name => $entry){
275           if(preg_match("/^ANSWER[0-9]*$/",$name)){
276             $ret[$name] = $entry;
277           }
278         }
279       }
280     }
281     return($ret);
282   }
285   /*! \brief  Returns an entry containing all requested ids.
286     @param  Array   The IDs of the entries we want to return.
287     @return Array   Of the requested entries. 
288    */
289   public function get_entries_by_id($ids)
290   {
291     if(!is_array($ids)){
292       trigger_error("Requires an array as parameter.");
293       return;
294     }
295     $this->reset_error();
297     $ret = array();
299     $xml_msg = "<xml>
300       <header>gosa_query_jobdb</header>
301       <target>GOSA</target>
302       <source>GOSA</source>
303       <where>
304       <clause>
305       <connector>or</connector>";
306     foreach($ids as $id){
307       $xml_msg .= "<phrase>
308         <operator>eq</operator>
309         <id>".$id."</id>
310         </phrase>";
311     }
312     $xml_msg .= "</clause>
313       </where>
314       </xml>";
316     if($this->connect()){
317       $this->o_sock->write($xml_msg);
318       $str = trim($this->o_sock->read());
319       $entries = $this->xml_to_array($str); 
320       if(isset($entries['XML'])){
321         foreach($entries['XML'] as $name => $entry){
322           if(preg_match("/^ANSWER[0-9]*$/",$name)){
323             $ret[$name] = $entry;
324           }
325         }
326       }
327     }
328     return($ret);
329   }
332   /*! \brief  Checks if the given id is in use.
333     @param  Integer The ID of the entry.
334     @return Boolean TRUE if entry exists. 
335    */
336   public function id_exists($id)
337   {
338     if(!is_numeric($id)){
339       trigger_error("Requires an integer as parameter.");
340       return;
341     }
343     $this->reset_error();
345     $xml_msg = "<xml>
346       <header>gosa_query_jobdb</header>
347       <target>GOSA</target>
348       <source>GOSA</source>
349       <where>
350       <clause>
351       <phrase>
352       <operator>eq</operator>
353       <id>".$id."</id>
354       </phrase>
355       </clause>
356       </where>
357       </xml>";
359     if($this->connect()){
360       $this->o_sock->write($xml_msg);
361       $str = trim($this->o_sock->read());
362       $entries = $this->xml_to_array($str); 
363       if( isset($entries['XML']['HEADER']) && 
364           $entries['XML']['HEADER']=="answer" && 
365           isset($entries['XML']['ANSWER1'])){
366         return(TRUE);
367       }
368     }
369     return(FALSE);
370   }
373   /*! \brief  Returns an entry from the gosaSupportQueue
374     @param  Integer The ID of the entry we want to return.
375     @return Array   Of the requested entry. 
376    */
377   public function get_entry_by_id($id)
378   {
379     if(!is_numeric($id)){
380       trigger_error("Requires an integer as parameter.");
381       return;
382     }
383     $this->reset_error();
384   
385     $ret = array();
386     $xml_msg = "<xml>
387       <header>gosa_query_jobdb</header>
388       <target>GOSA</target>
389       <source>GOSA</source>
390       <where>
391       <clause>
392       <phrase>
393       <operator>eq</operator>
394       <id>".$id."</id>
395       </phrase>
396       </clause>
397       </where>
398       </xml>";
399     if($this->connect()){
400       $this->o_sock->write($xml_msg);
401       $str = trim($this->o_sock->read());
402       $entries = $this->xml_to_array($str); 
403       if( isset($entries['XML']['HEADER']) &&
404           $entries['XML']['HEADER']=="answer" &&
405           isset($entries['XML']['ANSWER1'])){
406         $ret = $entries['XML']['ANSWER1'];
407       }
408     }
409     return($ret);
410   }
413   /*! \brief  Removes a set of entries from the GOsa support queue. 
414     @param  Array The IDs to remove.
415     @return Boolean True on success.
416    */
417   public function remove_entries($ids)
418   {
419     if(!is_array($ids)){
420       trigger_error("Requires an array as parameter.");
421       return;
422     }
424     $this->reset_error();
426     $ret = array();
428     $xml_msg = "<xml>
429       <header>gosa_delete_jobdb_entry</header>
430       <target>GOSA</target>
431       <source>GOSA</source>
432       <where>
433       <clause>
434       <connector>or</connector>";
435     foreach($ids as $id){
436       $xml_msg .= "<phrase>
437         <operator>eq</operator>
438         <id>".$id."</id>
439         </phrase>";
440     }
441     $xml_msg .= "</clause>
442       </where>
443       </xml>";
445     if($this->connect()){
446       $this->o_sock->write($xml_msg);
447       $str = $this->o_sock->read();
448       $entries = $this->xml_to_array($str);
449       if(isset($entries['XML']) || isset($entries['COUNT'])){
450         new log("debug","DaemonEvent (IDS) ", "gosaSupportDaemon::remove_entries()", $ids,"SUCCESS");
451         return(TRUE);
452       }else{
453         new log("debug","DaemonEvent (IDS) ", "gosaSupportDaemon::remove_entries()", $ids,"FAILED ".$this->get_error());
454       }
455     }
456     return(FALSE);
457   }
461   /*! \brief  Removes an entry from the GOsa support queue. 
462     @param  Integer The ID of the entry we want to remove.
463     @return Boolean True on success.
464    */
465   public function remove_entry($id)
466   {
467     return($this->remove_entries(array($id)));
468   }
471   /*! \brief  Parses the given xml string into an array 
472     @param  String XML string  
473     @return Array Returns an array containing the xml structure. 
474    */
475   private function xml_to_array($xml)
476   {
477     $params = array();
478     $level = array();
479     $parser  = xml_parser_create_ns();
480     xml_parse_into_struct($parser, $xml, $vals, $index);
482     $err_id = xml_get_error_code($parser);
483     if($err_id){
484       xml_parser_free($parser);
485     }else{
486       xml_parser_free($parser);
488       foreach ($vals as $xml_elem) {
489         if ($xml_elem['type'] == 'open') {
490           if (array_key_exists('attributes',$xml_elem)) {
491             list($level[$xml_elem['level']],$extra) = array_values($xml_elem['attributes']);
492           } else {
493             $level[$xml_elem['level']] = $xml_elem['tag'];
494           }
495         }
496         if ($xml_elem['type'] == 'complete') {
497           $start_level = 1;
498           $php_stmt = '$params';
499           while($start_level < $xml_elem['level']) {
500             $php_stmt .= '[$level['.$start_level.']]';
501             $start_level++;
502           }
503           $php_stmt .= '[$xml_elem[\'tag\']] = $xml_elem[\'value\'];';
504           @eval($php_stmt);
505         }
506       }
507     }
509     if(!isset($params['XML'])){
510       if (!array_key_exists('XML', $params)){
511         $this->set_error(_("Could not parse XML."));
512       }
513       $params = array("COUNT" => 0);
514     }
516     return($params); 
517   }
520   /*! \brief  Updates an entry with a set of new values, 
521     @param  Integer The ID of the entry, we want to update.
522     @param  Array   The variables to update.   
523     @return Boolean Returns TRUE on success. 
524    */
525   public function update_entries($ids,$data)
526   {
527     $this->reset_error();
528     if(!is_array($ids)){
529       trigger_error("Requires an array as first parameter.");
530       return;
531     }
533     if(!is_array($data)){
534       trigger_error("Requires an array as second parameter.");
535       return;
536     }
538     $attr = "";
539     foreach($data as $key => $value){
540       if(is_array($value)){
541         foreach($value as $sub_value){
542           $attr.= "<$key>".strtolower($sub_value)."</$key>\n";
543         }
544       }else{
545         $attr.= "<$key>".strtolower($value)."</$key>\n";
546       }
547     }
549     $xml_msg = "<xml>
550       <header>gosa_update_status_jobdb_entry</header>
551       <target>GOSA</target>
552       <source>GOSA</source>
553       <where>
554       <clause>
555       <connector>or</connector>";
556     foreach($ids as $id){
557       $xml_msg .= "<phrase>
558         <operator>eq</operator>
559         <id>".$id."</id>
560         </phrase>";
561     }
562     $xml_msg .= "</clause>
563       </where>
564       <update>
565       ".$attr." 
566       </update>
567       </xml>";
569     if($this->connect()){
571       $this->o_sock->write($xml_msg);
572       $str      = trim($this->o_sock->read());
573       $entries = $this->xml_to_array($str);
574       if(isset($entries['XML'])){
575         if(isset($entries['XML']['ERROR_STRING'])) {
576           $this->set_error($entries['XML']['ERROR_STRING']);
577           new log("debug","DaemonEvent (IDS) ", "gosaSupportDaemon::update_entries()", $ids,"FAILED setting (".$attr.") error was ".$this->get_error());
578           return(FALSE);
579         }
580         new log("debug","DaemonEvent (IDS) ", "gosaSupportDaemon::update_entries()", $ids,"SUCCESS");
581         return(TRUE);
582       }
583     }
584     return(FALSE);
585   }
588   /*! \brief  Returns the number of currently queued objects.
589       @return Integer  
590    */
591   public function number_of_queued_entries()
592   {
593     $xml_msg ="<xml><header>gosa_count_jobdb</header><target>GOSA</target><source>GOSA</source></xml>";
594     $this->connect();
595     if($this->connect()){
596       $this->o_sock->write($xml_msg);
597       $str     = trim($this->o_sock->read());
598       $entries = $this->xml_to_array($str);
599       if(isset($entries['XML'])){
600         return($entries['XML']['COUNT']);
601       }
602     }
603     return(-1);
604   } 
607   public function send_data($header, $to, $data= array(), $answer_expected = FALSE)
608   {
609     $xml_message= "";
611     /* Prepare data */
612     foreach ($data as $key => $value){
613       if(is_array($value)){
614         foreach($value as $sub_val){
615           $xml_message.= "<$key>$sub_value</$key>";
616         }
617       }else{
618         $xml_message.= "<$key>$value</$key>";
619       }
620     }
622     /* Multiple targets? */
623     if (!is_array($to)){
624       $to_targets= array($to);
625     } else {
626       $to_targets= $to;
627     }
629     /* Build target strings */
630     $target ="";
631     foreach($to_targets as $to){
632       $target.= "<target>$to</target>";
633     }
635     return $this->_send("<xml><header>$header</header><source>GOSA</source>$target".$xml_message."</xml>",$answer_expected);
636   }
639   /* Allows simply appending a new DaemonEvent 
640    */
641   public function append($event)
642   {
643     if(!($event instanceof DaemonEvent)){
644       return(FALSE);
645     }
646   
647     $this->reset_error();
649     /* Add to queue if new 
650      */
651     if($event->is_new()){
653       $request_answer = FALSE;
654       if($event->get_type() == SCHEDULED_EVENT){
655         $action = $event->get_schedule_action();
656       }elseif($event->get_type() == TRIGGERED_EVENT){
657         $action = $event->get_trigger_action();
658       }else{
659         trigger_error("Unknown type of queue event given.");
660         return(FALSE);
661       }
663       /* Get event informations, like targets..
664        */
665       $targets    = $event->get_targets();
666       $data       = $event->save();
668       /* Append an entry for each target 
669        */
670       foreach($targets as $target){
671         $data['macaddress'] = $target;
672         $this->send_data($action,$target,$data,$request_answer);
674         if($this->is_error()){
675           return(FALSE);
676         }
677       }
678       return(TRUE);
679     }else{
681       /* Updated edited entry.
682        */
683       $id                 = $event->get_id();
684       $data               = $event->save();
685       return($this->update_entries(array($id),$data));
686     }
688     return(FALSE);
689   }
692 /*! \brief  Returns an array containing all queued entries.
693     @return Array All queued entries as an array.
694    */
695   public function _send($data, $answer_expected= FALSE)
696   {
697     $this->reset_error();
698     $ret = array();
700     if($this->connect()){
701       $this->o_sock->write($data);
702       if ($answer_expected){
703         $str = trim($this->o_sock->read());
704         $entries = $this->xml_to_array($str);
705         if(isset($entries['XML']) && is_array($entries['XML'])){
706           $ret = $entries;
707           if(isset($entries['XML']['ERROR_STRING'])) {
708             $this->set_error($entries['XML']['ERROR_STRING']);
709             new log("debug","DaemonEvent (IDS) ", "gosaSupportDaemon::_send()", array($data=>$data),"FAILED ".$this->get_error());
710           }else{
711             new log("debug","DaemonEvent (IDS) ", "gosaSupportDaemon::_send()", array($data=>$data),"SUCCESS");
712           }
713         }
714       }else{
715         new log("debug","DaemonEvent (IDS) ", "gosaSupportDaemon::_send()", array($data=>$data),"Fire & forget, not result.! ".$this->get_error());
716       }
717     }
718     return($ret);
719   }
722   static function send($header, $to, $data= array(), $answer_expected = FALSE)
723   {
724     $xml_message= "";
726     /* Get communication object */
727     $d= new gosaSupportDaemon(TRUE,10);
729     /* Prepare data */
730     foreach ($data as $key => $value){
731       if(is_array($value)){
732         foreach($value as $sub_val){
733           $xml_message.= "<$key>$sub_value</$key>";
734         }
735       }else{
736         $xml_message.= "<$key>$value</$key>";
737       }
738     }
740     /* Multiple targets? */
741     if (!is_array($to)){
742       $to_targets= array($to);
743     } else {
744       $to_targets= $to;
745     }
747     /* Build target strings */
748     $target ="";
749     foreach($to_targets as $to){
750       $target.= "<target>$to</target>";
751     }
753     return $d->_send("<xml><header>$header</header><source>GOSA</source>$target".$xml_message."</xml>",$answer_expected);
754   }
757   /*! \brief  Removes all jobs from the queue that are tiggered with a specific macAddress.
758       @param  String  $mac  The mac address for which we want to remove all jobs.      
759    */
760   function clean_queue_from_mac($mac)
761   {
762     global $config;
764     /* First of all we have to check which jobs are startet 
765      *  for $mac 
766      */
767     $xml_msg ="<xml><header>gosa_query_jobdb</header><target>GOSA</target><source>GOSA</source><where><clause><phrase><macaddress>".$mac."</macaddress></phrase></clause></where></xml>";  
768     
769     new log("debug","DaemonEvent ", "gosaSupportDaemon::clean_queue_from_mac()", array($mac => $mac)," start cleaning.");
770  
771     $data = $this->_send($xml_msg,TRUE);
772     if(is_array($data) && isset($data['XML'])){
773       $already_aborted = FALSE;
774       foreach($data['XML']  as $name => $entry){
775         if(preg_match("/answer[0-9]*/i",$name)){
776           $entry['STATUS'] = strtoupper($entry['STATUS']);
777           switch($entry['STATUS']){
779             case 'PROCESSING' :
781               /* Send abort event, but only once 
782                */
783               if($already_aborted){
784                 break;
785               }elseif(class_available("DaemonEvent_faireboot")){
786                 $already_aborted = TRUE;
787                 $tmp = new DaemonEvent_faireboot($config);
788                 $tmp->add_targets(array($mac));
789                 $tmp->set_type(TRIGGERED_EVENT);
790                 if(!$this->append($tmp)){
791                   msg_dialog::display(_("Error"), sprintf(_("Cannot send abort event for entry: %s"),$entry['ID']) , ERROR_DIALOG);
792                   new log("debug","DaemonEvent ", "gosaSupportDaemon::clean_queue_from_mac()", array($mac => $mac),
793                       "FAILED, could not send 'DaemonEvent_faireboot' for entry ID (".$entry['ID'].") - ".$this->get_error());
794                 }else{
795                   new log("debug","DaemonEvent ", "gosaSupportDaemon::clean_queue_from_mac()", array($mac => $mac),
796                       "SUCCESS, send 'DaemonEvent_faireboot' for entry ID (".$entry['ID'].")");
797                 }
798                 ;break;
799               }else{
800                 /* Couldn't find abort event, just remove entry */
801               }
803             case 'WAITING':
804             case 'ERROR':
805             default :
806             
807               /* Simply remove entries from queue. 
808                *  Failed or waiting events, can be removed without any trouble.
809                */ 
810               if(!$this->remove_entries(array($entry['ID']))){
811                 msg_dialog::display(_("Error"), sprintf(_("Cannot remove entry: %s"),$entry['ID']) , ERROR_DIALOG);
812               }
813               ;break;
814           }
815     
816         }
817       }
818     }
819   }
822 static function ping($target)
824   if (tests::is_mac($target)){
825     /* Get communication object */
826     $d= new gosaSupportDaemon(TRUE,0.5);
827     $answer= $d->_send("<xml><header>gosa_ping</header><source>GOSA</source><target>$target</target></xml>", TRUE);
828     return (count($answer) ? TRUE:FALSE);
829   }
831   return (FALSE);
836 // vim:tabstop=2:expandtab:shiftwidth=2:filetype=php:syntax:ruler:
837 ?>