c92488c37ef562d8ff9a0eff2968c8ca13ec7f92
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: class_plugin.inc 14584 2009-10-12 14:04:22Z hickert $$
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 management
24 {
25 // Public
26 public $config = null;
27 public $ui = null;
29 public $plIcon = "";
30 public $plDescription = "";
31 public $plHeadline = "";
33 // Protected
34 protected $dn = "";
35 protected $dns = array();
37 protected $storagePoints = array();
39 protected $last_dn = "";
40 protected $last_dns = array();
42 protected $tabClass = "";
43 protected $tabType = "";
44 protected $aclCategory = "";
45 protected $objectName = "";
46 protected $tabObject = null;
47 protected $dialogObject = null;
49 protected $last_tabObject = null;
50 protected $last_dialogObject = null;
52 protected $displayApplyBtn = "";
53 protected $cpHandler = null;
54 protected $cpPastingStarted = FALSE;
56 protected $snapHandler = null;
58 // Private
59 protected $plugname = "";
60 protected $headpage = null;
61 protected $filter = null;
62 protected $actions = array();
64 function __construct($config,$ui,$plugname, $headpage)
65 {
66 $this->plugname = $plugname;
67 $this->headpage = $headpage;
68 $this->ui = $ui;
69 $this->config = $config;
71 if($this->cpHandler) $this->headpage->setCopyPasteHandler($this->cpHandler);
72 if($this->snapHandler) $this->headpage->setSnapshotHandler($this->snapHandler);
74 if(empty($this->plIcon)){
75 $this->plIcon = "plugins/".$plugname."/images/plugin.png";
76 }
78 // Register default actions
79 $this->registerAction("new", "newEntry");
80 $this->registerAction("edit", "editEntry");
81 $this->registerAction("apply", "applyChanges");
82 $this->registerAction("save", "saveChanges");
83 $this->registerAction("cancel", "cancelEdit");
84 $this->registerAction("remove", "removeEntryRequested");
85 $this->registerAction("removeConfirmed", "removeEntryConfirmed");
87 $this->registerAction("copy", "copyPasteHandler");
88 $this->registerAction("cut", "copyPasteHandler");
89 $this->registerAction("paste", "copyPasteHandler");
91 $this->registerAction("snapshot", "createSnapshotDialog");
92 $this->registerAction("restore", "restoreSnapshotDialog");
93 $this->registerAction("saveSnapshot","saveSnapshot");
94 $this->registerAction("restoreSnapshot","restoreSnapshot");
95 $this->registerAction("cancelSnapshot","closeDialogs");
97 }
99 function execute()
100 {
101 // Ensure that html posts and gets are kept even if we see a 'Entry islocked' dialog.
102 $vars = array('/^act$/','/^listing/','/^PID$/','/^FILTER_PID$/');
103 session::set('LOCK_VARS_TO_USE',$vars);
105 /* Display the copy & paste dialog, if it is currently open */
106 $ret = $this->copyPasteHandler("",array());
107 if($ret){
108 return($this->getHeader().$ret);
109 }
111 // Update filter
112 if ($this->filter) {
113 $this->filter->update();
114 session::global_set(get_class($this)."_filter", $this->filter);
115 session::set('autocomplete', $this->filter);
116 if (!$this->filter->isValid()){
117 msg_dialog::display(_("Filter error"), _("The filter is incomplete!"), ERROR_DIALOG);
118 }
119 }
121 // Handle actions (POSTs and GETs)
122 $str = $this->handleActions($this->detectPostActions());
123 if($str) return($this->getHeader().$str);
125 // Open single dialog objects
126 if(is_object($this->dialogObject)){
127 if(method_exists($this->dialogObject,'save_object')) $this->dialogObject->save_object();
128 if(method_exists($this->dialogObject,'execute')){
129 $display = $this->dialogObject->execute();
130 $display.= $this->_getTabFooter();
131 return($this->getHeader().$display);
132 }
133 }
135 // Display tab object.
136 if($this->tabObject instanceOf tabs || $this->tabObject instanceOf multi_plug){
137 $this->tabObject->save_object();
138 $display = $this->tabObject->execute();
139 $display.= $this->_getTabFooter();
140 return($this->getHeader().$display);
141 }
143 // Set current restore base for snapshot handling.
144 if(is_object($this->snapHandler)){
145 $bases = array();
146 foreach($this->storagePoints as $sp){
147 $bases[] = $sp.$this->headpage->getBase();
148 }
149 $this->snapHandler->setSnapshotBases($bases);
150 }
152 $this->headpage->update();
153 $display = $this->headpage->render();
154 return($this->getHeader().$display);
155 }
157 protected function getHeader()
158 {
159 if (get_object_info() != ""){
160 $display= print_header(get_template_path($this->plIcon),_($this->plDescription),
161 "<img alt=\"\" class=\"center\" src=\"".get_template_path('images/lists/locked.png')."\">".
162 LDAP::fix(get_object_info()));
163 } else {
164 $display= print_header(get_template_path($this->plIcon),_($this->plDescription));
165 }
166 return($display);
167 }
170 protected function _getTabFooter()
171 {
172 if(!($this->tabObject instanceOf tabs || $this->tabObject instanceOf multi_plug)){
173 return("");
174 }
176 if($this->tabObject->by_object[$this->tabObject->current]){
177 $current = $this->tabObject->by_object[$this->tabObject->current];
178 if(is_object($current->dialog)){
179 return("");
180 }
181 }
183 $str = "";
184 if(isset($this->tabObject->read_only) && $this->tabObject->read_only == TRUE){
185 $str.= "<p style=\"text-align:right\">
186 <input type=submit name=\"edit_cancel\" value=\"".msgPool::cancelButton()."\">
187 </p>";
188 return($str);
189 }else{
190 $str.= "<p style=\"text-align:right\">\n";
191 $str.= "<input type=submit name=\"edit_finish\" style=\"width:80px\" value=\"".msgPool::okButton()."\">\n";
192 $str.= " \n";
193 if($this->displayApplyBtn){
194 $str.= "<input type=submit name=\"edit_apply\" value=\"".msgPool::applyButton()."\">\n";
195 $str.= " \n";
196 }
197 $str.= "<input type=submit name=\"edit_cancel\" value=\"".msgPool::cancelButton()."\">\n";
198 $str.= "</p>";
199 }
200 return($str);
201 }
204 protected function removeEntryRequested($action,$entry,$all)
205 {
206 $disallowed = array();
207 $this->dns = array();
208 foreach($entry as $dn){
209 $acl = $this->ui->get_permissions($dn, $this->aclCategory."/".$this->aclPlugin);
210 if(preg_match("/d/",$acl)){
211 $this->dns[] = $dn;
212 }else{
213 $disallowed[] = $dn;
214 }
215 }
217 if(count($disallowed)){
218 msg_dialog::display(_("Permission"),msgPool::permDelete($disallowed),INFO_DIALOG);
219 }
221 if(count($this->dns)){
223 /* Check locks */
224 if ($user= get_multiple_locks($this->dns)){
225 return(gen_locked_message($user,$this->dns));
226 }
228 $dns_names = array();
229 foreach($this->dns as $dn){
230 $dns_names[] =LDAP::fix($dn);
231 }
232 add_lock ($this->dns, $this->ui->dn);
234 /* Lock the current entry, so nobody will edit it during deletion */
235 $smarty = get_smarty();
236 $smarty->assign("info", msgPool::deleteInfo($dns_names,_($this->objectName)));
237 $smarty->assign("multiple", true);
238 return($smarty->fetch(get_template_path('remove.tpl', TRUE)));
239 }
240 }
243 protected function removeEntryConfirmed()
244 {
245 foreach($this->dns as $key => $dn){
247 /* Load permissions for selected 'dn' and check if
248 we're allowed to remove this 'dn' */
249 $acl = $this->ui->get_permissions($dn, $this->aclCategory."/".$this->aclPlugin);
250 if(preg_match("/d/",$acl)){
252 /* Delete request is permitted, perform LDAP action */
253 $this->dn = $dn;
254 $tab = $this->tabClass;
255 $this->tabObject= new $tab($this->config,$this->config->data['TABS'][$this->tabType], $this->dn, $this->aclCategory);
256 $this->tabObject->set_acl_base($this->dn);
257 $this->tabObject->delete ();
258 del_lock($this->dn);
259 } else {
261 /* Normally this shouldn't be reached, send some extra
262 logs to notify the administrator */
263 msg_dialog::display(_("Permission error"), msgPool::permDelete(), ERROR_DIALOG);
264 new log("security","groups/".get_class($this),$dn,array(),"Tried to trick deletion.");
265 }
266 }
268 $this->remove_lock();
269 $this->closeDialogs();
270 }
273 function detectPostActions()
274 {
275 $action= $this->headpage->getAction();
277 if(isset($_POST['edit_apply'])) $action['action'] = "apply";
278 if(isset($_POST['edit_finish'])) $action['action'] = "save";
279 if(isset($_POST['edit_cancel'])) $action['action'] = "cancel";
280 if(isset($_POST['delete_confirmed'])) $action['action'] = "removeConfirmed";
282 // Detect Snapshot actions
283 if(isset($_POST['CreateSnapshot'])) $action['action'] = "saveSnapshot";
284 if(isset($_POST['CancelSnapshot'])) $action['action'] = "cancelSnapshot";
285 foreach($_POST as $name => $value){
286 $once =TRUE;
287 if(preg_match("/^RestoreSnapShot_/",$name) && $once){
288 $once = FALSE;
289 $entry = base64_decode(preg_replace("/^RestoreSnapShot_([^_]*)_[xy]$/i","\\1",$name));
290 $action['action'] = "restoreSnapshot";
291 $action['targets'] = array($entry);
292 }
293 }
295 return($action);
296 }
298 function handleActions($action)
299 {
300 // Start action
301 if(isset($this->actions[$action['action']])){
302 $func = $this->actions[$action['action']];
303 if(!isset($action['targets']))$action['targets']= array();
304 return($this->$func($action['action'],$action['targets'],$action));
305 }
306 }
308 function createSnapshotDialog($action="",$target=array(),$all=array())
309 {
310 foreach($target as $entry){
311 if(!empty($entry) && $this->ui->allow_snapshot_create($entry,$this->aclCategory)){
312 $this->dialogObject = new SnapShotDialog($this->config,$entry,$this);
313 $this->dialogObject->aclCategories = array($this->aclCategory);
315 }else{
316 msg_dialog::display(_("Permission"),sprintf(_("You are not allowed to create a snapshot for %s."),$entry),
317 ERROR_DIALOG);
318 }
319 }
320 }
323 /*! \brief Creates a snapshot new entry - This method is called when the somebody
324 * clicks 'save' in the "Create snapshot dialog" (see ::createSnapshotDialog).
325 *
326 * @param String 'action' The name of the action which was the used as trigger.
327 * @param Array 'target' A list of object dns, which should be affected by this method.
328 * @param Array 'all' A combination of both 'action' and 'target'.
329 */
330 function saveSnapsho($action="",$target=array(),$all=array())
331 {
332 $this->dialogObject->save_object();
333 $msgs = $this->dialogObject->check();
334 if(count($msgs)){
335 foreach($msgs as $msg){
336 msg_dialog::display(_("Error"), $msg, ERROR_DIALOG);
337 }
338 }else{
339 $this->dn = $this->dialogObject->dn;
340 $this->snapHandler->create_snapshot( $this->dn,$this->dialogObject->CurrentDescription);
341 $this->closeDialogs();
342 }
343 }
346 /*! \brief Restores a snapshot object.
347 * The dn of the snapshot entry has to be given as ['target'] parameter.
348 *
349 * @param String 'action' The name of the action which was the used as trigger.
350 * @param Array 'target' A list of object dns, which should be affected by this method.
351 * @param Array 'all' A combination of both 'action' and 'target'.
352 */
353 function restoreSnapshot($action="",$target=array(),$all=array())
354 {
355 $entry = array_pop($target);
356 if(!empty($entry) && $this->ui->allow_snapshot_restore($entry,$this->aclCategory)){
357 $this->snapHandler->restore_snapshot($entry);
358 $this->closeDialogs();
359 }else{
360 msg_dialog::display(_("Permission"),sprintf(_("You are not allowed to restore a snapshot for %s."),$entry),
361 ERROR_DIALOG);
362 }
363 }
366 /*! \brief Displays the "Restore snapshot dialog" for a given target.
367 * If no target is specified, open the restore removed object
368 * dialog.
369 * @param String 'action' The name of the action which was the used as trigger.
370 * @param Array 'target' A list of object dns, which should be affected by this method.
371 * @param Array 'all' A combination of both 'action' and 'target'.
372 */
373 function restoreSnapshotDialog($action="",$target=array(),$all=array())
374 {
375 // Set current restore base for snapshot handling.
376 if(is_object($this->snapHandler)){
377 $bases = array();
378 foreach($this->storagePoints as $sp){
379 $bases[] = $sp.$this->headpage->getBase();
380 }
381 }
383 // No target, open the restore removed object dialog.
384 if(!count($target)){
385 $entry = $this->headpage->getBase();
386 if(!empty($entry) && $this->ui->allow_snapshot_restore($entry,$this->aclCategory)){
387 $this->dialogObject = new SnapShotDialog($this->config,$entry,$this);
388 $this->dialogObject->set_snapshot_bases($bases);
389 $this->dialogObject->display_all_removed_objects = true;
390 $this->dialogObject->display_restore_dialog = true;
391 }else{
392 msg_dialog::display(_("Permission"),sprintf(_("You are not allowed to restore a snapshot for %s."),$entry),
393 ERROR_DIALOG);
394 }
395 }else{
397 // Display the restore points for a given object.
398 $entry = array_pop($target);
399 if(!empty($entry) && $this->ui->allow_snapshot_restore($entry,$this->aclCategory)){
400 $this->dialogObject = new SnapShotDialog($this->config,$entry,$this);
401 $this->dialogObject->set_snapshot_bases($bases);
402 $this->dialogObject->display_restore_dialog = true;
403 }else{
404 msg_dialog::display(_("Permission"),sprintf(_("You are not allowed to restore a snapshot for %s."),$entry),
405 ERROR_DIALOG);
406 }
407 }
408 }
411 /*! \brief This method intiates the object creation.
412 *
413 * @param String 'action' The name of the action which was the used as trigger.
414 * @param Array 'target' A list of object dns, which should be affected by this method.
415 * @param Array 'all' A combination of both 'action' and 'target'.
416 */
417 function newEntry($action="",$target=array(),$all=array(), $altTabClass ="", $altTabType = "", $altAclCategory)
418 {
419 /* To handle mutliple object types overload this method.
420 * ...
421 * registerAction('newUser', 'newEntry');
422 * registerAction('newGroup','newEntry');
423 * ...
424 *
425 * function newEntry($action="",$target=array(),$all=array(), $altTabClass ="", $altTabType = "", $altAclCategory)
426 * {
427 * switch($action){
428 * case 'newUser' : {
429 * mangement::newEntry($action,$target,$all,"usertabs","USERTABS","users");
430 * }
431 * case 'newGroup' : {
432 * mangement::newEntry($action,$target,$all,"grouptabs","GROUPTABS","groups");
433 * }
434 * }
435 * }
436 **/
437 $tabType = $this->tabType;
438 $tabClass = $this->tabClass;
439 $aclCategory = $this->aclCategory;
440 if(!empty($altTabClass)) $tabClass = $altTabClass;
441 if(!empty($altTabType)) $tabType = $altTabType;
442 if(!empty($altAclCategory)) $aclCategory = $altAclCategory;
443 {
444 // Check locking & lock entry if required
445 $this->displayApplyBtn = FALSE;
446 $this->dn = "new";
447 $this->is_new = TRUE;
448 $this->is_single_edit = FALSE;
449 $this->is_multiple_edit = FALSE;
451 set_object_info($this->dn);
453 // Open object.
454 if(empty($tabClass) || empty($tabType)){
455 // No tab type defined
456 }else{
457 $this->tabObject= new $tabClass($this->config,$this->config->data['TABS'][$tabType], $this->dn, $aclCategory);
458 $this->tabObject->set_acl_base($this->headpage->getBase());
459 }
460 }
463 /*! \brief This method opens an existing object or a list of existing objects to be edited.
464 *
465 *
466 * @param String 'action' The name of the action which was the used as trigger.
467 * @param Array 'target' A list of object dns, which should be affected by this method.
468 * @param Array 'all' A combination of both 'action' and 'target'.
469 */
470 function editEntry($action="",$target=array(),$all=array(), $altTabClass ="", $altTabType = "", $altAclCategory)
471 {
472 /* To handle mutliple object types overload this method.
473 * ...
474 * registerAction('editUser', 'editEntry');
475 * registerAction('editGroup','editEntry');
476 * ...
477 *
478 * function editEntry($action="",$target=array(),$all=array(), $altTabClass ="", $altTabType = "", $altAclCategory)
479 * {
480 * switch($action){
481 * case 'editUser' : {
482 * mangement::editEntry($action,$target,$all,"usertabs","USERTABS","users");
483 * }
484 * case 'editGroup' : {
485 * mangement::editEntry($action,$target,$all,"grouptabs","GROUPTABS","groups");
486 * }
487 * }
488 * }
489 **/
490 $tabType = $this->tabType;
491 $tabClass = $this->tabClass;
492 $aclCategory = $this->aclCategory;
493 if(!empty($altTabClass)) $tabClass = $altTabClass;
494 if(!empty($altTabType)) $tabType = $altTabType;
495 if(!empty($altAclCategory)) $aclCategory = $altAclCategory;
497 // Single edit - we only got one object dn.
498 if(count($target) == 1){
499 $this->displayApplyBtn = TRUE;
500 $this->is_new = FALSE;
501 $this->is_single_edit = TRUE;
502 $this->is_multiple_edit = FALSE;
504 // Get the dn of the object and creates lock
505 $this->dn = array_pop($target);
506 set_object_info($this->dn);
507 $user = get_lock($this->dn);
508 if ($user != ""){
509 return(gen_locked_message ($user, $this->dn,TRUE));
510 }
511 add_lock ($this->dn, $this->ui->dn);
513 // Open object.
514 if(empty($tabClass) || empty($tabType)){
515 trigger_error("We can't edit any object(s). 'tabClass' or 'tabType' is empty!");
516 }else{
517 $tab = $tabClass;
518 $this->tabObject= new $tab($this->config,$this->config->data['TABS'][$tabType], $this->dn,$aclCategory);
519 $this->tabObject->set_acl_base($this->dn);
520 }
521 }else{
523 // We've multiple entries to edit.
524 $this->is_new = FALSE;
525 $this->is_singel_edit = FALSE;
526 $this->is_multiple_edit = TRUE;
528 // Open multiple edit handler.
529 if(empty($tabClass) || empty($tabType)){
530 trigger_error("We can't edit any object(s). 'tabClass' or 'tabType' is empty!");
531 }else{
532 $this->dns = $target;
533 $tmp = new multi_plug($this->config,$tabClass,$this->config->data['TABS'][$tabType],
534 $this->dns,$this->headpage->getBase(),$aclCategory);
536 // Check for locked entries
537 if ($tmp->entries_locked()){
538 return($tmp->display_lock_message());
539 }
541 // Now lock entries.
542 $tmp->lock_entries($this->ui->dn);
543 if($tmp->multiple_available()){
544 $this->tabObject = $tmp;
545 set_object_info($this->tabObject->get_object_info());
546 }
547 }
548 }
549 }
552 /*! \brief Save object modifications and closes dialogs (returns to object listing).
553 * - Calls '::check' to validate the given input.
554 * - Calls '::save' to save back object modifications (e.g. to ldap).
555 * - Calls '::remove_locks' to remove eventually created locks.
556 * - Calls '::closeDialogs' to return to the object listing.
557 */
558 protected function saveChanges()
559 {
560 if($this->tabObject instanceOf tabs || $this->tabObject instanceOf multi_plug){
561 $this->tabObject->save_object();
562 $msgs = $this->tabObject->check();
563 if(count($msgs)){
564 msg_dialog::displayChecks($msgs);
565 return("");
566 }else{
567 $this->tabObject->save();
568 $this->remove_lock();
569 $this->closeDialogs();
570 }
571 }
572 }
575 /*! \brief Save object modifications and keep dialogs opened.
576 * - Calls '::check' to validate the given input.
577 * - Calls '::save' to save back object modifications (e.g. to ldap).
578 */
579 protected function applyChanges()
580 {
581 if($this->tabObject instanceOf tabs || $this->tabObject instanceOf multi_plug){
582 $this->tabObject->save_object();
583 $msgs = $this->tabObject->check();
584 if(count($msgs)){
585 msg_dialog::displayChecks($msgs);
586 return("");
587 }else{
588 $this->tabObject->save();
589 $this->tabObject->re_init();
590 }
591 }
592 }
595 /*! \brief This method closes dialogs
596 * and cleans up the cached object info and the ui.
597 */
598 protected function closeDialogs()
599 {
600 $this->last_dn = $this->dn;
601 $this->last_dns = $this->dns;
602 $this->last_tabObject = $this->tabObject;
603 $this->last_dialogObject = $this->dialogObject;
604 $this->dn = "";
605 $this->dns = array();
606 $this->tabObject = null;
607 $this->dialogObject = null;
608 set_object_info();
609 }
612 /*! \brief Editing an object was caneled.
613 * Close dialogs/tabs and remove locks.
614 */
615 protected function cancelEdit()
616 {
617 $this->remove_lock();
618 $this->closeDialogs();
619 }
622 /*! \brief Every click in the list user interface sends an event
623 * here can we connect those events to a method.
624 * eg. ::registerEvent('new','createUser')
625 * When the action/event new is send, the method 'createUser'
626 * will be called.
627 */
628 function registerAction($action,$target)
629 {
630 $this->actions[$action] = $target;
631 }
634 /*! \brief Removes ldap object locks created by this class.
635 * Whenever an object is edited, we create locks to avoid
636 * concurrent modifications.
637 * This locks will automatically removed here.
638 */
639 function remove_lock()
640 {
641 if(!empty($this->dn) && $this->dn != "new"){
642 del_lock($this->dn);
643 }
644 if(count($this->dns)){
645 del_lock($this->dns);
646 }
647 }
650 /*! \brief This method is used to queue and process copy&paste actions.
651 * Allows to copy, cut and paste mutliple entries at once.
652 * @param String 'action' The name of the action which was the used as trigger.
653 * @param Array 'target' A list of object dns, which should be affected by this method.
654 * @param Array 'all' A combination of both 'action' and 'target'.
655 */
656 function copyPasteHandler($action="",$target=array(),$all=array())
657 {
658 // Return without any actions while copy&paste handler is disabled.
659 if(!is_object($this->cpHandler)) return("");
661 // Save user input
662 $this->cpHandler->save_object();
664 // Add entries to queue
665 if($s_action == "copy" || $s_action == "cut"){
666 $this->cpHandler->cleanup_queue();
667 foreach($s_entry as $dn){
668 if($s_action == "copy" && $this->ui->is_copyable($dn,$this->aclCategory,$this->aclPlugin)){
669 $this->cpHandler->add_to_queue($dn,"copy",$this->tabClass,$this->tabType,$this->aclCategory);
670 }
671 if($s_action == "cut" && $this->ui->is_cutable($dn,$this->aclCategory,$this->aclPlugin)){
672 $this->cpHandler->add_to_queue($dn,"cut",$this->tabClass,$this->tabType,$this->aclCategory);
673 }
674 }
675 }
677 // Initiate pasting
678 if($s_action == "paste"){
679 $this->cpPastingStarted = TRUE;
680 }
682 // Display any c&p dialogs, eg. object modifications required before pasting.
683 if($this->cpPastingStarted && $this->cpHandler->entries_queued()){
684 $this->cpHandler->SetVar("base",$this->headpage->getBase());
685 $data = $this->cpHandler->execute();
686 if(!empty($data)){
687 return($data);
688 }
689 }
691 // Automatically disable pasting process since there is no entry left to paste.
692 if(!$this->cpHandler->entries_queued()){
693 $this->cpPastingStarted = FALSE;
694 }
695 return("");
696 }
699 }
701 // vim:tabstop=2:expandtab:shiftwidth=2:filetype=php:syntax:ruler:
702 ?>