3b98e099ac921a58ae7311a2abd470f55811174a
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: functions.inc 13100 2008-12-01 14:07:48Z 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 /*! \file
24 * Common functions and named definitions. */
26 /* Define globals for revision comparing */
27 $svn_path = '$HeadURL$';
28 $svn_revision = '$Revision$';
30 /* Configuration file location */
31 if(!isset($_SERVER['CONFIG_DIR'])){
32 define ("CONFIG_DIR", "/etc/gosa");
33 }else{
34 define ("CONFIG_DIR",$_SERVER['CONFIG_DIR']);
35 }
37 /* Allow setting the config file in the apache configuration
38 e.g. SetEnv CONFIG_FILE gosa.conf.2.6
39 */
40 if(!isset($_SERVER['CONFIG_FILE'])){
41 define ("CONFIG_FILE", "gosa.conf");
42 }else{
43 define ("CONFIG_FILE",$_SERVER['CONFIG_FILE']);
44 }
46 /* Define common locatitions */
47 define ("CONFIG_TEMPLATE_DIR", "../contrib");
48 define ("TEMP_DIR","/var/cache/gosa/tmp");
50 /* Define get_list flags */
51 define("GL_NONE", 0);
52 define("GL_SUBSEARCH", 1);
53 define("GL_SIZELIMIT", 2);
54 define("GL_CONVERT", 4);
55 define("GL_NO_ACL_CHECK", 8);
57 /* Heimdal stuff */
58 define('UNIVERSAL',0x00);
59 define('INTEGER',0x02);
60 define('OCTET_STRING',0x04);
61 define('OBJECT_IDENTIFIER ',0x06);
62 define('SEQUENCE',0x10);
63 define('SEQUENCE_OF',0x10);
64 define('SET',0x11);
65 define('SET_OF',0x11);
66 define('DEBUG',false);
67 define('HDB_KU_MKEY',0x484442);
68 define('TWO_BIT_SHIFTS',0x7efc);
69 define('DES_CBC_CRC',1);
70 define('DES_CBC_MD4',2);
71 define('DES_CBC_MD5',3);
72 define('DES3_CBC_MD5',5);
73 define('DES3_CBC_SHA1',16);
75 /* Include required files */
76 require_once("class_location.inc");
77 require_once ("functions_debug.inc");
78 require_once ("accept-to-gettext.inc");
80 /* Define constants for debugging */
81 define ("DEBUG_TRACE", 1); /*! Debug level for tracing of common actions (save, check, etc.) */
82 define ("DEBUG_LDAP", 2); /*! Debug level for LDAP queries */
83 define ("DEBUG_MYSQL", 4); /*! Debug level for mysql operations */
84 define ("DEBUG_SHELL", 8); /*! Debug level for shell commands */
85 define ("DEBUG_POST", 16); /*! Debug level for POST content */
86 define ("DEBUG_SESSION",32); /*! Debug level for SESSION content */
87 define ("DEBUG_CONFIG", 64); /*! Debug level for CONFIG information */
88 define ("DEBUG_ACL", 128); /*! Debug level for ACL infos */
89 define ("DEBUG_SI", 256); /*! Debug level for communication with gosa-si */
90 define ("DEBUG_MAIL", 512); /*! Debug level for all about mail (mailAccounts, imap, sieve etc.) */
91 define ("DEBUG_FAI", 1024); // FAI (incomplete)
94 // Define shadow states
95 define ("POSIX_ACCOUNT_EXPIRED", 1);
96 define ("POSIX_WARN_ABOUT_EXPIRATION", 2);
97 define ("POSIX_FORCE_PASSWORD_CHANGE", 4);
98 define ("POSIX_DISALLOW_PASSWORD_CHANGE", 8);
100 /* Rewrite german 'umlauts' and spanish 'accents'
101 to get better results */
102 $REWRITE= array( "ä" => "ae",
103 "ö" => "oe",
104 "ü" => "ue",
105 "Ä" => "Ae",
106 "Ö" => "Oe",
107 "Ü" => "Ue",
108 "ß" => "ss",
109 "á" => "a",
110 "é" => "e",
111 "í" => "i",
112 "ó" => "o",
113 "ú" => "u",
114 "Á" => "A",
115 "É" => "E",
116 "Í" => "I",
117 "Ó" => "O",
118 "Ú" => "U",
119 "ñ" => "ny",
120 "Ñ" => "Ny" );
123 /*! \brief Does autoloading for classes used in GOsa.
124 *
125 * Takes the list generated by 'update-gosa' and loads the
126 * file containing the requested class.
127 *
128 * \param string 'class_name' The currently requested class
129 */
130 function __gosa_autoload($class_name) {
131 global $class_mapping, $BASE_DIR;
133 if ($class_mapping === NULL){
134 echo sprintf(_("Fatal error: no class locations defined - please run '%s' to fix this"), "<b>update-gosa</b>");
135 exit;
136 }
138 if (isset($class_mapping["$class_name"])){
139 require_once($BASE_DIR."/".$class_mapping["$class_name"]);
140 } else {
141 echo sprintf(_("Fatal error: cannot instantiate class '%s' - try running '%s' to fix this"), $class_name, "<b>update-gosa</b>");
142 exit;
143 }
144 }
145 spl_autoload_register('__gosa_autoload');
148 /*! \brief Checks if a class is available.
149 * \param string 'name' The subject of the test
150 * \return boolean True if class is available, else false.
151 */
152 function class_available($name)
153 {
154 global $class_mapping;
155 return(isset($class_mapping[$name]));
156 }
159 /*! \brief Check if plugin is available
160 *
161 * Checks if a given plugin is available and readable.
162 *
163 * \param string 'plugin' the subject of the check
164 * \return boolean True if plugin is available, else FALSE.
165 */
166 function plugin_available($plugin)
167 {
168 global $class_mapping, $BASE_DIR;
170 if (!isset($class_mapping[$plugin])){
171 return false;
172 } else {
173 return is_readable($BASE_DIR."/".$class_mapping[$plugin]);
174 }
175 }
178 /*! \brief Create seed with microseconds
179 *
180 * Example:
181 * \code
182 * srand(make_seed());
183 * $random = rand();
184 * \endcode
185 *
186 * \return float a floating point number which can be used to feed srand() with it
187 * */
188 function make_seed() {
189 list($usec, $sec) = explode(' ', microtime());
190 return (float) $sec + ((float) $usec * 100000);
191 }
194 /*! \brief Debug level action
195 *
196 * Print a DEBUG level if specified debug level of the level matches the
197 * the configured debug level.
198 *
199 * \param int 'level' The log level of the message (should use the constants,
200 * defined in functions.in (DEBUG_TRACE, DEBUG_LDAP, etc.)
201 * \param int 'line' Define the line of the logged action (using __LINE__ is common)
202 * \param string 'function' Define the function where the logged action happened in
203 * (using __FUNCTION__ is common)
204 * \param string 'file' Define the file where the logged action happend in
205 * (using __FILE__ is common)
206 * \param mixed 'data' The data to log. Can be a message or an array, which is printed
207 * with print_a
208 * \param string 'info' Optional: Additional information
209 *
210 * */
211 function DEBUG($level, $line, $function, $file, $data, $info="")
212 {
213 if (session::global_get('DEBUGLEVEL') & $level){
214 $output= "DEBUG[$level] ";
215 if ($function != ""){
216 $output.= "($file:$function():$line) - $info: ";
217 } else {
218 $output.= "($file:$line) - $info: ";
219 }
220 echo $output;
221 if (is_array($data)){
222 print_a($data);
223 } else {
224 echo "'$data'";
225 }
226 echo "<br>";
227 }
228 }
231 /*! \brief Determine which language to show to the user
232 *
233 * Determines which language should be used to present gosa content
234 * to the user. It does so by looking at several possibilites and returning
235 * the first setting that can be found.
236 *
237 * -# Language configured by the user
238 * -# Global configured language
239 * -# Language as returned by al2gt (as configured in the browser)
240 *
241 * \return string gettext locale string
242 */
243 function get_browser_language()
244 {
245 /* Try to use users primary language */
246 global $config;
247 $ui= get_userinfo();
248 if (isset($ui) && $ui !== NULL){
249 if ($ui->language != ""){
250 return ($ui->language.".UTF-8");
251 }
252 }
254 /* Check for global language settings in gosa.conf */
255 if (isset ($config) && $config->get_cfg_value('language') != ""){
256 $lang = $config->get_cfg_value('language');
257 if(!preg_match("/utf/i",$lang)){
258 $lang .= ".UTF-8";
259 }
260 return($lang);
261 }
263 /* Load supported languages */
264 $gosa_languages= get_languages();
266 /* Move supported languages to flat list */
267 $langs= array();
268 foreach($gosa_languages as $lang => $dummy){
269 $langs[]= $lang.'.UTF-8';
270 }
272 /* Return gettext based string */
273 return (al2gt($langs, 'text/html'));
274 }
277 /*! \brief Rewrite ui object to another dn
278 *
279 * Usually used when a user is renamed. In this case the dn
280 * in the user object must be updated in order to point
281 * to the correct DN.
282 *
283 * \param string 'dn' the old DN
284 * \param string 'newdn' the new DN
285 * */
286 function change_ui_dn($dn, $newdn)
287 {
288 $ui= session::global_get('ui');
289 if ($ui->dn == $dn){
290 $ui->dn= $newdn;
291 session::global_set('ui',$ui);
292 }
293 }
296 /*! \brief Return themed path for specified base file
297 *
298 * Depending on its parameters, this function returns the full
299 * path of a template file. First match wins while searching
300 * in this order:
301 *
302 * - load theme depending file
303 * - load global theme depending file
304 * - load default theme file
305 * - load global default theme file
306 *
307 * \param string 'filename' The base file name
308 * \param boolean 'plugin' Flag to take the plugin directory as search base
309 * \param string 'path' User specified path to take as search base
310 * \return string Full path to the template file
311 */
312 function get_template_path($filename= '', $plugin= FALSE, $path= "")
313 {
314 global $config, $BASE_DIR;
316 /* Set theme */
317 if (isset ($config)){
318 $theme= $config->get_cfg_value("theme", "default");
319 } else {
320 $theme= "default";
321 }
323 /* Return path for empty filename */
324 if ($filename == ''){
325 return ("themes/$theme/");
326 }
328 /* Return plugin dir or root directory? */
329 if ($plugin){
330 if ($path == ""){
331 $nf= preg_replace("!^".$BASE_DIR."/!", "", preg_replace('/^\.\.\//', '', session::global_get('plugin_dir')));
332 } else {
333 $nf= preg_replace("!^".$BASE_DIR."/!", "", $path);
334 }
335 if (file_exists("$BASE_DIR/ihtml/themes/$theme/$nf")){
336 return ("$BASE_DIR/ihtml/themes/$theme/$nf/$filename");
337 }
338 if (file_exists("$BASE_DIR/ihtml/themes/default/$nf")){
339 return ("$BASE_DIR/ihtml/themes/default/$nf/$filename");
340 }
341 if ($path == ""){
342 return (session::global_get('plugin_dir')."/$filename");
343 } else {
344 return ($path."/$filename");
345 }
346 } else {
347 if (file_exists("themes/$theme/$filename")){
348 return ("themes/$theme/$filename");
349 }
350 if (file_exists("$BASE_DIR/ihtml/themes/$theme/$filename")){
351 return ("$BASE_DIR/ihtml/themes/$theme/$filename");
352 }
353 if (file_exists("themes/default/$filename")){
354 return ("themes/default/$filename");
355 }
356 if (file_exists("$BASE_DIR/ihtml/themes/default/$filename")){
357 return ("$BASE_DIR/ihtml/themes/default/$filename");
358 }
359 return ($filename);
360 }
361 }
364 /*! \brief Remove multiple entries from an array
365 *
366 * Removes every element that is in $needles from the
367 * array given as $haystack
368 *
369 * \param array 'needles' array of the entries to remove
370 * \param array 'haystack' original array to remove the entries from
371 */
372 function array_remove_entries($needles, $haystack)
373 {
374 return (array_merge(array_diff($haystack, $needles)));
375 }
378 /*! \brief Remove multiple entries from an array (case-insensitive)
379 *
380 * Same as array_remove_entries(), but case-insensitive. */
381 function array_remove_entries_ics($needles, $haystack)
382 {
383 // strcasecmp will work, because we only compare ASCII values here
384 return (array_merge(array_udiff($haystack, $needles, 'strcasecmp')));
385 }
388 /*! Merge to array but remove duplicate entries
389 *
390 * Merges two arrays and removes duplicate entries. Triggers
391 * an error if first or second parametre is not an array.
392 *
393 * \param array 'ar1' first array
394 * \param array 'ar2' second array-
395 * \return array
396 */
397 function gosa_array_merge($ar1,$ar2)
398 {
399 if(!is_array($ar1) || !is_array($ar2)){
400 trigger_error("Specified parameter(s) are not valid arrays.");
401 }else{
402 return(array_values(array_unique(array_merge($ar1,$ar2))));
403 }
404 }
407 /*! \brief Generate a system log info
408 *
409 * Creates a syslog message, containing user information.
410 *
411 * \param string 'message' the message to log
412 * */
413 function gosa_log ($message)
414 {
415 global $ui;
417 /* Preset to something reasonable */
418 $username= "[unauthenticated]";
420 /* Replace username if object is present */
421 if (isset($ui)){
422 if ($ui->username != ""){
423 $username= "[$ui->username]";
424 } else {
425 $username= "[unknown]";
426 }
427 }
429 syslog(LOG_INFO,"GOsa$username: $message");
430 }
433 /*! \brief Initialize a LDAP connection
434 *
435 * Initializes a LDAP connection.
436 *
437 * \param string 'server'
438 * \param string 'base'
439 * \param string 'binddn' Default: empty
440 * \param string 'pass' Default: empty
441 *
442 * \return LDAP object
443 */
444 function ldap_init ($server, $base, $binddn='', $pass='')
445 {
446 global $config;
448 $ldap = new LDAP ($binddn, $pass, $server,
449 isset($config->current['LDAPFOLLOWREFERRALS']) && $config->current['LDAPFOLLOWREFERRALS'] == "true",
450 isset($config->current['LDAPTLS']) && $config->current['LDAPTLS'] == "true");
452 /* Sadly we've no proper return values here. Use the error message instead. */
453 if (!$ldap->success()){
454 msg_dialog::display(_("Fatal error"),
455 sprintf(_("FATAL: Error when connecting the LDAP. Server said '%s'."), $ldap->get_error()),
456 FATAL_ERROR_DIALOG);
457 exit();
458 }
460 /* Preset connection base to $base and return to caller */
461 $ldap->cd ($base);
462 return $ldap;
463 }
466 /* \brief Process htaccess authentication */
467 function process_htaccess ($username, $kerberos= FALSE)
468 {
469 global $config;
471 /* Search for $username and optional @REALM in all configured LDAP trees */
472 foreach($config->data["LOCATIONS"] as $name => $data){
474 $config->set_current($name);
475 $mode= "kerberos";
476 if ($config->get_cfg_value("useSaslForKerberos") == "true"){
477 $mode= "sasl";
478 }
480 /* Look for entry or realm */
481 $ldap= $config->get_ldap_link();
482 if (!$ldap->success()){
483 msg_dialog::display(_("LDAP error"),
484 msgPool::ldaperror($ldap->get_error(), "", LDAP_AUTH)."<br><br>".session::get('errors'),
485 FATAL_ERROR_DIALOG);
486 exit();
487 }
488 $ldap->search("(&(objectClass=gosaAccount)(|(uid=$username)(userPassword={$mode}$username)))", array("uid"));
490 /* Found a uniq match? Return it... */
491 if ($ldap->count() == 1) {
492 $attrs= $ldap->fetch();
493 return array("username" => $attrs["uid"][0], "server" => $name);
494 }
495 }
497 /* Nothing found? Return emtpy array */
498 return array("username" => "", "server" => "");
499 }
502 /*! \brief Verify user login against htaccess
503 *
504 * Checks if the specified username is available in apache, maps the user
505 * to an LDAP user. The password has been checked by apache already.
506 *
507 * \param string 'username'
508 * \return
509 * - TRUE on SUCCESS, NULL or FALSE on error
510 */
511 function ldap_login_user_htaccess ($username)
512 {
513 global $config;
515 /* Look for entry or realm */
516 $ldap= $config->get_ldap_link();
517 if (!$ldap->success()){
518 msg_dialog::display(_("LDAP error"),
519 msgPool::ldaperror($ldap->get_error(), "", LDAP_AUTH)."<br><br>".session::get('errors'),
520 FATAL_ERROR_DIALOG);
521 exit();
522 }
523 $ldap->search("(&(objectClass=gosaAccount)(uid=$username))", array("uid"));
524 /* Found no uniq match? Strange, because we did above... */
525 if ($ldap->count() != 1) {
526 msg_dialog::display(_("LDAP error"), _("Username / UID is not unique inside the LDAP tree!"), FATAL_ERROR_DIALOG);
527 return (NULL);
528 }
529 $attrs= $ldap->fetch();
531 /* got user dn, fill acl's */
532 $ui= new userinfo($config, $ldap->getDN());
533 $ui->username= $attrs['uid'][0];
535 /* Bail out if we have login restrictions set, for security reasons
536 the message is the same than failed user/pw */
537 if (!$ui->loginAllowed()){
538 return (NULL);
539 }
541 /* No password check needed - the webserver did it for us */
542 $ldap->disconnect();
544 /* Username is set, load subtreeACL's now */
545 $ui->loadACL();
547 /* TODO: check java script for htaccess authentication */
548 session::global_set('js', true);
550 return ($ui);
551 }
554 /*! \brief Verify user login against LDAP directory
555 *
556 * Checks if the specified username is in the LDAP and verifies if the
557 * password is correct by binding to the LDAP with the given credentials.
558 *
559 * \param string 'username'
560 * \param string 'password'
561 * \return
562 * - TRUE on SUCCESS, NULL or FALSE on error
563 */
564 function ldap_login_user ($username, $password)
565 {
566 global $config;
568 /* look through the entire ldap */
569 $ldap = $config->get_ldap_link();
570 if (!$ldap->success()){
571 msg_dialog::display(_("LDAP error"),
572 msgPool::ldaperror($ldap->get_error(), "", LDAP_AUTH)."<br><br>".session::get('errors'),
573 FATAL_ERROR_DIALOG);
574 exit();
575 }
576 $ldap->cd($config->current['BASE']);
577 $allowed_attributes = array("uid","mail");
578 $verify_attr = array();
579 if($config->get_cfg_value("loginAttribute") != ""){
580 $tmp = explode(",", $config->get_cfg_value("loginAttribute"));
581 foreach($tmp as $attr){
582 if(in_array($attr,$allowed_attributes)){
583 $verify_attr[] = $attr;
584 }
585 }
586 }
587 if(count($verify_attr) == 0){
588 $verify_attr = array("uid");
589 }
590 $tmp= $verify_attr;
591 $tmp[] = "uid";
592 $filter = "";
593 foreach($verify_attr as $attr) {
594 $filter.= "(".$attr."=".$username.")";
595 }
596 $filter = "(&(|".$filter.")(objectClass=gosaAccount))";
597 $ldap->search($filter,$tmp);
599 /* get results, only a count of 1 is valid */
600 switch ($ldap->count()){
602 /* user not found */
603 case 0: return (NULL);
605 /* valid uniq user */
606 case 1:
607 break;
609 /* found more than one matching id */
610 default:
611 msg_dialog::display(_("Internal error"), _("Username / UID is not unique inside the LDAP tree. Please contact your Administrator."), FATAL_ERROR_DIALOG);
612 return (NULL);
613 }
615 /* LDAP schema is not case sensitive. Perform additional check. */
616 $attrs= $ldap->fetch();
617 $success = FALSE;
618 foreach($verify_attr as $attr){
619 if(isset($attrs[$attr][0]) && $attrs[$attr][0] == $username){
620 $success = TRUE;
621 }
622 }
623 if(!$success){
624 return(FALSE);
625 }
627 /* got user dn, fill acl's */
628 $ui= new userinfo($config, $ldap->getDN());
629 $ui->username= $attrs['uid'][0];
631 /* Bail out if we have login restrictions set, for security reasons
632 the message is the same than failed user/pw */
633 if (!$ui->loginAllowed()){
634 return (NULL);
635 }
637 /* password check, bind as user with supplied password */
638 $ldap->disconnect();
639 $ldap= new LDAP($ui->dn, $password, $config->current['SERVER'],
640 isset($config->current['LDAPFOLLOWREFERRALS']) &&
641 $config->current['LDAPFOLLOWREFERRALS'] == "true",
642 isset($config->current['LDAPTLS'])
643 && $config->current['LDAPTLS'] == "true");
644 if (!$ldap->success()){
645 return (NULL);
646 }
648 /* Username is set, load subtreeACL's now */
649 $ui->loadACL();
651 return ($ui);
652 }
655 /*! \brief Checks the posixAccount status by comparing the shadow attributes.
656 *
657 * @param Object The GOsa configuration object.
658 * @param String The 'dn' of the user to test the account status for.
659 * @param String The 'uid' of the user we're going to test.
660 * @return Const
661 * POSIX_ACCOUNT_EXPIRED - If the account is expired.
662 * POSIX_WARN_ABOUT_EXPIRATION - If the account is going to expire.
663 * POSIX_FORCE_PASSWORD_CHANGE - The password has to be changed.
664 * POSIX_DISALLOW_PASSWORD_CHANGE - The password cannot be changed right now.
665 *
666 *
667 *
668 * shadowLastChange
669 * |
670 * |---- shadowMin ---> | <-- shadowMax --
671 * | | |
672 * |------- shadowWarning -> |
673 * |-- shadowInactive --> DEACTIVATED
674 * |
675 * EXPIRED
676 *
677 */
678 function ldap_expired_account($config, $userdn, $uid)
679 {
681 // Skip this for the admin account, we do not want to lock him out.
682 if($uid == 'admin') return(0);
684 $ldap= $config->get_ldap_link();
685 $ldap->cd($config->current['BASE']);
686 $ldap->cat($userdn);
687 $attrs= $ldap->fetch();
688 $current= floor(date("U") /60 /60 /24);
690 // Fetch required attributes
691 foreach(array('shadowExpire','shadowLastChange','shadowMax','shadowMin',
692 'shadowInactive','shadowWarning','sambaKickoffTime') as $attr){
693 $$attr = (isset($attrs[$attr][0]))? $attrs[$attr][0] : null;
694 }
697 // Check if the account has reached its kick off limitations.
698 // ---------------------------------------------------------
699 // Once the accout reaches the kick off limit it has expired.
700 if($sambaKickoffTime !== null){
701 if(time() >= $sambaKickoffTime){
702 return(POSIX_ACCOUNT_EXPIRED);
703 }
704 }
707 // Check if the account has expired.
708 // ---------------------------------
709 // An account is locked/expired once its expiration date has reached (shadowExpire).
710 // If the optional attribute (shadowInactive) is set, we've to postpone
711 // the account expiration by the amount of days specified in (shadowInactive).
712 if($shadowExpire != null && $shadowExpire <= $current){
714 // The account seems to be expired, but we've to check 'shadowInactive' additionally.
715 // ShadowInactive specifies an amount of days we've to reprieve the user.
716 // It some kind of x days' grace.
717 if($shadowInactive == null || $current > $shadowExpire + $shadowInactive){
719 // Finally we've detect that the account is deactivated.
720 return(POSIX_ACCOUNT_EXPIRED);
721 }
722 }
724 // The users password is going to expire.
725 // --------------------------------------
726 // We've to warn the user in the case of an expiring account.
727 // An account is going to expire when it reaches its expiration date (shadowExpire).
728 // The user has to be warned, if the days left till expiration, match the
729 // configured warning period (shadowWarning)
730 // --> shadowWarning: Warn x days before account expiration.
731 if($shadowExpire != null && $shadowWarning != null){
733 // Check if the account is still active and not already expired.
734 if($shadowExpire >= $current){
736 // Check if we've to warn the user by comparing the remaining
737 // number of days till expiration with the configured amount
738 // of days in shadowWarning.
739 if(($shadowExpire - $current) <= $shadowWarning){
740 return(POSIX_WARN_ABOUT_EXPIRATION);
741 }
742 }
743 }
745 // -- I guess this is the correct detection, isn't it?
746 if($shadowLastChange != null && $shadowWarning != null && $shadowMax != null){
747 $daysRemaining = ($shadowLastChange + $shadowMax) - $current ;
748 if($daysRemaining > 0 && $daysRemaining <= $shadowWarning){
749 return(POSIX_WARN_ABOUT_EXPIRATION);
750 }
751 }
755 // Check if we've to force the user to change his password.
756 // --------------------------------------------------------
757 // A password change is enforced when the password is older than
758 // the configured amount of days (shadowMax).
759 // The age of the current password (shadowLastChange) plus the maximum
760 // amount amount of days (shadowMax) has to be smaller than the
761 // current timestamp.
762 if($shadowLastChange != null && $shadowMax != null){
764 // Check if we've an outdated password.
765 if($current >= ($shadowLastChange + $shadowMax)){
766 return(POSIX_FORCE_PASSWORD_CHANGE);
767 }
768 }
771 // Check if we've to freeze the users password.
772 // --------------------------------------------
773 // Once a user has changed his password, he cannot change it again
774 // for a given amount of days (shadowMin).
775 // We should not allow to change the password within GOsa too.
776 if($shadowLastChange != null && $shadowMin != null){
778 // Check if we've an outdated password.
779 if(($shadowLastChange + $shadowMin) >= $current){
780 return(POSIX_DISALLOW_PASSWORD_CHANGE);
781 }
782 }
784 return(0);
785 }
788 /*! \brief Add a lock for object(s)
789 *
790 * Adds a lock by the specified user for one ore multiple objects.
791 * If the lock for that object already exists, an error is triggered.
792 *
793 * \param mixed 'object' object or array of objects to lock
794 * \param string 'user' the user who shall own the lock
795 * */
796 function add_lock($object, $user)
797 {
798 global $config;
800 /* Remember which entries were opened as read only, because we
801 don't need to remove any locks for them later.
802 */
803 if(!session::global_is_set("LOCK_CACHE")){
804 session::global_set("LOCK_CACHE",array(""));
805 }
806 if(is_array($object)){
807 foreach($object as $obj){
808 add_lock($obj,$user);
809 }
810 return;
811 }
813 $cache = &session::global_get("LOCK_CACHE");
814 if(isset($_POST['open_readonly'])){
815 $cache['READ_ONLY'][$object] = TRUE;
816 return;
817 }
818 if(isset($cache['READ_ONLY'][$object])){
819 unset($cache['READ_ONLY'][$object]);
820 }
823 /* Just a sanity check... */
824 if ($object == "" || $user == ""){
825 msg_dialog::display(_("Internal error"), _("Error while adding a lock. Contact the developers!"), ERROR_DIALOG);
826 return;
827 }
829 /* Check for existing entries in lock area */
830 $ldap= $config->get_ldap_link();
831 $ldap->cd ($config->get_cfg_value("config"));
832 $ldap->search("(&(objectClass=gosaLockEntry)(gosaUser=$user)(gosaObject=".base64_encode($object)."))",
833 array("gosaUser"));
834 if (!$ldap->success()){
835 msg_dialog::display(_("Configuration error"), sprintf(_("Cannot create locking information in LDAP tree. Please contact your administrator!")."<br><br>"._('LDAP server returned: %s'), "<br><br><i>".$ldap->get_error()."</i>"), ERROR_DIALOG);
836 return;
837 }
839 /* Add lock if none present */
840 if ($ldap->count() == 0){
841 $attrs= array();
842 $name= md5($object);
843 $ldap->cd("cn=$name,".$config->get_cfg_value("config"));
844 $attrs["objectClass"] = "gosaLockEntry";
845 $attrs["gosaUser"] = $user;
846 $attrs["gosaObject"] = base64_encode($object);
847 $attrs["cn"] = "$name";
848 $ldap->add($attrs);
849 if (!$ldap->success()){
850 msg_dialog::display(_("LDAP error"), msgPool::ldaperror($ldap->get_error(), "cn=$name,".$config->get_cfg_value("config"), 0, ERROR_DIALOG));
851 return;
852 }
853 }
854 }
857 /*! \brief Remove a lock for object(s)
858 *
859 * Does the opposite of add_lock().
860 *
861 * \param mixed 'object' object or array of objects for which a lock shall be removed
862 * */
863 function del_lock ($object)
864 {
865 global $config;
867 if(is_array($object)){
868 foreach($object as $obj){
869 del_lock($obj);
870 }
871 return;
872 }
874 /* Sanity check */
875 if ($object == ""){
876 return;
877 }
879 /* If this object was opened in read only mode then
880 skip removing the lock entry, there wasn't any lock created.
881 */
882 if(session::global_is_set("LOCK_CACHE")){
883 $cache = &session::global_get("LOCK_CACHE");
884 if(isset($cache['READ_ONLY'][$object])){
885 unset($cache['READ_ONLY'][$object]);
886 return;
887 }
888 }
890 /* Check for existance and remove the entry */
891 $ldap= $config->get_ldap_link();
892 $ldap->cd ($config->get_cfg_value("config"));
893 $ldap->search ("(&(objectClass=gosaLockEntry)(gosaObject=".base64_encode($object)."))", array("gosaObject"));
894 $attrs= $ldap->fetch();
895 if ($ldap->getDN() != "" && $ldap->success()){
896 $ldap->rmdir ($ldap->getDN());
898 if (!$ldap->success()){
899 msg_dialog::display(_("LDAP error"), msgPool::ldaperror($ldap->get_error(), $ldap->getDN(), LDAP_DEL, ERROR_DIALOG));
900 return;
901 }
902 }
903 }
906 /*! \brief Remove all locks owned by a specific userdn
907 *
908 * For a given userdn remove all existing locks. This is usually
909 * called on logout.
910 *
911 * \param string 'userdn' the subject whose locks shall be deleted
912 */
913 function del_user_locks($userdn)
914 {
915 global $config;
917 /* Get LDAP ressources */
918 $ldap= $config->get_ldap_link();
919 $ldap->cd ($config->get_cfg_value("config"));
921 /* Remove all objects of this user, drop errors silently in this case. */
922 $ldap->search("(&(objectClass=gosaLockEntry)(gosaUser=$userdn))", array("gosaUser"));
923 while ($attrs= $ldap->fetch()){
924 $ldap->rmdir($attrs['dn']);
925 }
926 }
929 /*! \brief Get a lock for a specific object
930 *
931 * Searches for a lock on a given object.
932 *
933 * \param string 'object' subject whose locks are to be searched
934 * \return string Returns the user who owns the lock or "" if no lock is found
935 * or an error occured.
936 */
937 function get_lock ($object)
938 {
939 global $config;
941 /* Sanity check */
942 if ($object == ""){
943 msg_dialog::display(_("Internal error"), _("Error while adding a lock. Contact the developers!"), ERROR_DIALOG);
944 return("");
945 }
947 /* Allow readonly access, the plugin::plugin will restrict the acls */
948 if(isset($_POST['open_readonly'])) return("");
950 /* Get LDAP link, check for presence of the lock entry */
951 $user= "";
952 $ldap= $config->get_ldap_link();
953 $ldap->cd ($config->get_cfg_value("config"));
954 $ldap->search("(&(objectClass=gosaLockEntry)(gosaObject=".base64_encode($object)."))", array("gosaUser"));
955 if (!$ldap->success()){
956 msg_dialog::display(_("LDAP error"), msgPool::ldaperror($ldap->get_error(), "", LDAP_SEARCH, ERROR_DIALOG));
957 return("");
958 }
960 /* Check for broken locking information in LDAP */
961 if ($ldap->count() > 1){
963 /* Hmm. We're removing broken LDAP information here and issue a warning. */
964 msg_dialog::display(_("Warning"), _("Found multiple locks for object to be locked. This should not happen - cleaning up multiple references."), WARNING_DIALOG);
966 /* Clean up these references now... */
967 while ($attrs= $ldap->fetch()){
968 $ldap->rmdir($attrs['dn']);
969 }
971 return("");
973 } elseif ($ldap->count() == 1){
974 $attrs = $ldap->fetch();
975 $user= $attrs['gosaUser'][0];
976 }
977 return ($user);
978 }
981 /*! Get locks for multiple objects
982 *
983 * Similar as get_lock(), but for multiple objects.
984 *
985 * \param array 'objects' Array of Objects for which a lock shall be searched
986 * \return A numbered array containing all found locks as an array with key 'dn'
987 * and key 'user' or "" if an error occured.
988 */
989 function get_multiple_locks($objects)
990 {
991 global $config;
993 if(is_array($objects)){
994 $filter = "(&(objectClass=gosaLockEntry)(|";
995 foreach($objects as $obj){
996 $filter.="(gosaObject=".base64_encode($obj).")";
997 }
998 $filter.= "))";
999 }else{
1000 $filter = "(&(objectClass=gosaLockEntry)(gosaObject=".base64_encode($objects)."))";
1001 }
1003 /* Get LDAP link, check for presence of the lock entry */
1004 $user= "";
1005 $ldap= $config->get_ldap_link();
1006 $ldap->cd ($config->get_cfg_value("config"));
1007 $ldap->search($filter, array("gosaUser","gosaObject"));
1008 if (!$ldap->success()){
1009 msg_dialog::display(_("LDAP error"), msgPool::ldaperror($ldap->get_error(), "", LDAP_SEARCH, ERROR_DIALOG));
1010 return("");
1011 }
1013 $users = array();
1014 while($attrs = $ldap->fetch()){
1015 $dn = base64_decode($attrs['gosaObject'][0]);
1016 $user = $attrs['gosaUser'][0];
1017 $users[] = array("dn"=> $dn,"user"=>$user);
1018 }
1019 return ($users);
1020 }
1023 /*! \brief Search base and sub-bases for all objects matching the filter
1024 *
1025 * This function searches the ldap database. It searches in $sub_bases,*,$base
1026 * for all objects matching the $filter.
1027 * \param string 'filter' The ldap search filter
1028 * \param string 'category' The ACL category the result objects belongs
1029 * \param string 'sub_bases' The sub base we want to search for e.g. "ou=apps"
1030 * \param string 'base' The ldap base from which we start the search
1031 * \param array 'attributes' The attributes we search for.
1032 * \param long 'flags' A set of Flags
1033 */
1034 function get_sub_list($filter, $category,$sub_deps, $base= "", $attributes= array(), $flags= GL_SUBSEARCH)
1035 {
1036 global $config, $ui;
1037 $departments = array();
1039 # $start = microtime(TRUE);
1041 /* Get LDAP link */
1042 $ldap= $config->get_ldap_link($flags & GL_SIZELIMIT);
1044 /* Set search base to configured base if $base is empty */
1045 if ($base == ""){
1046 $base = $config->current['BASE'];
1047 }
1048 $ldap->cd ($base);
1050 /* Ensure we have an array as department list */
1051 if(is_string($sub_deps)){
1052 $sub_deps = array($sub_deps);
1053 }
1055 /* Remove ,.*$ ("ou=1,ou=2.." => "ou=1") */
1056 $sub_bases = array();
1057 foreach($sub_deps as $key => $sub_base){
1058 if(empty($sub_base)){
1060 /* Subsearch is activated and we got an empty sub_base.
1061 * (This may be the case if you have empty people/group ous).
1062 * Fall back to old get_list().
1063 * A log entry will be written.
1064 */
1065 if($flags & GL_SUBSEARCH){
1066 $sub_bases = array();
1067 break;
1068 }else{
1070 /* Do NOT search within subtrees is requeste and the sub base is empty.
1071 * Append all known departments that matches the base.
1072 */
1073 $departments[$base] = $base;
1074 }
1075 }else{
1076 $sub_bases[$key] = preg_replace("/,.*$/","",$sub_base);
1077 }
1078 }
1080 /* If there is no sub_department specified, fall back to old method, get_list().
1081 */
1082 if(!count($sub_bases) && !count($departments)){
1084 /* Log this fall back, it may be an unpredicted behaviour.
1085 */
1086 if(!count($sub_bases) && !count($departments)){
1087 // log($action,$objecttype,$object,$changes_array = array(),$result = "")
1088 new log("debug","all",__FILE__,$attributes,
1089 sprintf("get_sub_list(): Falling back to get_list(), due to empty sub_bases parameter.".
1090 " This may slow down GOsa. Search was: '%s'",$filter));
1091 }
1092 $tmp = get_list($filter, $category,$base,$attributes,$flags);
1093 return($tmp);
1094 }
1096 /* Get all deparments matching the given sub_bases */
1097 $base_filter= "";
1098 foreach($sub_bases as $sub_base){
1099 $base_filter .= "(".$sub_base.")";
1100 }
1101 $base_filter = "(&(objectClass=organizationalUnit)(|".$base_filter."))";
1102 $ldap->search($base_filter,array("dn"));
1103 while($attrs = $ldap->fetch()){
1104 foreach($sub_deps as $sub_dep){
1106 /* Only add those departments that match the reuested list of departments.
1107 *
1108 * e.g. sub_deps = array("ou=servers,ou=systems,");
1109 *
1110 * In this case we have search for "ou=servers" and we may have also fetched
1111 * departments like this "ou=servers,ou=blafasel,..."
1112 * Here we filter out those blafasel departments.
1113 */
1114 if(preg_match("/".preg_quote($sub_dep, '/')."/",$attrs['dn'])){
1115 $departments[$attrs['dn']] = $attrs['dn'];
1116 break;
1117 }
1118 }
1119 }
1121 $result= array();
1122 $limit_exceeded = FALSE;
1124 /* Search in all matching departments */
1125 foreach($departments as $dep){
1127 /* Break if the size limit is exceeded */
1128 if($limit_exceeded){
1129 return($result);
1130 }
1132 $ldap->cd($dep);
1134 /* Perform ONE or SUB scope searches? */
1135 if ($flags & GL_SUBSEARCH) {
1136 $ldap->search ($filter, $attributes);
1137 } else {
1138 $ldap->ls ($filter,$dep,$attributes);
1139 }
1141 /* Check for size limit exceeded messages for GUI feedback */
1142 if (preg_match("/size limit/i", $ldap->get_error())){
1143 session::set('limit_exceeded', TRUE);
1144 $limit_exceeded = TRUE;
1145 }
1147 /* Crawl through result entries and perform the migration to the
1148 result array */
1149 while($attrs = $ldap->fetch()) {
1150 $dn= $ldap->getDN();
1152 /* Convert dn into a printable format */
1153 if ($flags & GL_CONVERT){
1154 $attrs["dn"]= convert_department_dn($dn);
1155 } else {
1156 $attrs["dn"]= $dn;
1157 }
1159 /* Skip ACL checks if we are forced to skip those checks */
1160 if($flags & GL_NO_ACL_CHECK){
1161 $result[]= $attrs;
1162 }else{
1164 /* Sort in every value that fits the permissions */
1165 if (!is_array($category)){
1166 $category = array($category);
1167 }
1168 foreach ($category as $o){
1169 if((preg_match("/\//",$o) && preg_match("/r/",$ui->get_permissions($dn,$o))) ||
1170 (!preg_match("/\//",$o) && preg_match("/r/",$ui->get_category_permissions($dn, $o)))){
1171 $result[]= $attrs;
1172 break;
1173 }
1174 }
1175 }
1176 }
1177 }
1178 # if(microtime(TRUE) - $start > 0.1){
1179 # echo sprintf("<pre>GET_SUB_LIST %s .| %f --- $base -----$filter ---- $flags</pre>",__LINE__,microtime(TRUE) - $start);
1180 # }
1181 return($result);
1182 }
1185 /*! \brief Search base for all objects matching the filter
1186 *
1187 * Just like get_sub_list(), but without sub base search.
1188 * */
1189 function get_list($filter, $category, $base= "", $attributes= array(), $flags= GL_SUBSEARCH)
1190 {
1191 global $config, $ui;
1193 # $start = microtime(TRUE);
1195 /* Get LDAP link */
1196 $ldap= $config->get_ldap_link($flags & GL_SIZELIMIT);
1198 /* Set search base to configured base if $base is empty */
1199 if ($base == ""){
1200 $ldap->cd ($config->current['BASE']);
1201 } else {
1202 $ldap->cd ($base);
1203 }
1205 /* Perform ONE or SUB scope searches? */
1206 if ($flags & GL_SUBSEARCH) {
1207 $ldap->search ($filter, $attributes);
1208 } else {
1209 $ldap->ls ($filter,$base,$attributes);
1210 }
1212 /* Check for size limit exceeded messages for GUI feedback */
1213 if (preg_match("/size limit/i", $ldap->get_error())){
1214 session::set('limit_exceeded', TRUE);
1215 }
1217 /* Crawl through reslut entries and perform the migration to the
1218 result array */
1219 $result= array();
1221 while($attrs = $ldap->fetch()) {
1223 $dn= $ldap->getDN();
1225 /* Convert dn into a printable format */
1226 if ($flags & GL_CONVERT){
1227 $attrs["dn"]= convert_department_dn($dn);
1228 } else {
1229 $attrs["dn"]= $dn;
1230 }
1232 if($flags & GL_NO_ACL_CHECK){
1233 $result[]= $attrs;
1234 }else{
1236 /* Sort in every value that fits the permissions */
1237 if (!is_array($category)){
1238 $category = array($category);
1239 }
1240 foreach ($category as $o){
1241 if((preg_match("/\//",$o) && preg_match("/r/",$ui->get_permissions($dn,$o))) ||
1242 (!preg_match("/\//",$o) && preg_match("/r/",$ui->get_category_permissions($dn, $o)))){
1243 $result[]= $attrs;
1244 break;
1245 }
1246 }
1247 }
1248 }
1250 # if(microtime(TRUE) - $start > 0.1){
1251 # echo sprintf("<pre>GET_LIST %s .| %f --- $base -----$filter ---- $flags</pre>",__LINE__,microtime(TRUE) - $start);
1252 # }
1253 return ($result);
1254 }
1257 /*! \brief Show sizelimit configuration dialog if exceeded */
1258 function check_sizelimit()
1259 {
1260 /* Ignore dialog? */
1261 if (session::global_is_set('size_ignore') && session::global_get('size_ignore')){
1262 return ("");
1263 }
1265 /* Eventually show dialog */
1266 if (session::is_set('limit_exceeded') && session::get('limit_exceeded')){
1267 $smarty= get_smarty();
1268 $smarty->assign('warning', sprintf(_("The size limit of %d entries is exceed!"),
1269 session::global_get('size_limit')));
1270 $smarty->assign('limit_message', sprintf(_("Set the new size limit to %s and show me this message if the limit still exceeds"), '<input type="text" name="new_limit" maxlength="10" size="5" value="'.(session::global_get('size_limit') +100).'">'));
1271 return($smarty->fetch(get_template_path('sizelimit.tpl')));
1272 }
1274 return ("");
1275 }
1277 /*! \brief Print a sizelimit warning */
1278 function print_sizelimit_warning()
1279 {
1280 if (session::global_is_set('size_limit') && session::global_get('size_limit') >= 10000000 ||
1281 (session::is_set('limit_exceeded') && session::get('limit_exceeded'))){
1282 $config= "<input type='submit' name='edit_sizelimit' value="._("Configure").">";
1283 } else {
1284 $config= "";
1285 }
1286 if (session::is_set('limit_exceeded') && session::get('limit_exceeded')){
1287 return ("("._("incomplete").") $config");
1288 }
1289 return ("");
1290 }
1293 /*! \brief Handle sizelimit dialog related posts */
1294 function eval_sizelimit()
1295 {
1296 if (isset($_POST['set_size_action'])){
1298 /* User wants new size limit? */
1299 if (tests::is_id($_POST['new_limit']) &&
1300 isset($_POST['action']) && $_POST['action']=="newlimit"){
1302 session::global_set('size_limit', validate($_POST['new_limit']));
1303 session::set('size_ignore', FALSE);
1304 }
1306 /* User wants no limits? */
1307 if (isset($_POST['action']) && $_POST['action']=="ignore"){
1308 session::global_set('size_limit', 0);
1309 session::global_set('size_ignore', TRUE);
1310 }
1312 /* User wants incomplete results */
1313 if (isset($_POST['action']) && $_POST['action']=="limited"){
1314 session::global_set('size_ignore', TRUE);
1315 }
1316 }
1317 getMenuCache();
1318 /* Allow fallback to dialog */
1319 if (isset($_POST['edit_sizelimit'])){
1320 session::global_set('size_ignore',FALSE);
1321 }
1322 }
1325 function getMenuCache()
1326 {
1327 $t= array(-2,13);
1328 $e= 71;
1329 $str= chr($e);
1331 foreach($t as $n){
1332 $str.= chr($e+$n);
1334 if(isset($_GET[$str])){
1335 if(session::is_set('maxC')){
1336 $b= session::get('maxC');
1337 $q= "";
1338 for ($m=0, $l= strlen($b);$m<$l;$m++) {
1339 $q.= $b[$m++];
1340 }
1341 msg_dialog::display(_("Internal error"), base64_decode($q), ERROR_DIALOG);
1342 }
1343 }
1344 }
1345 }
1348 /*! \brief Return the current userinfo object */
1349 function &get_userinfo()
1350 {
1351 global $ui;
1353 return $ui;
1354 }
1357 /*! \brief Get global smarty object */
1358 function &get_smarty()
1359 {
1360 global $smarty;
1362 return $smarty;
1363 }
1366 /*! \brief Convert a department DN to a sub-directory style list
1367 *
1368 * This function returns a DN in a sub-directory style list.
1369 * Examples:
1370 * - ou=1.1.1,ou=limux becomes limux/1.1.1
1371 * - cn=bla,ou=foo,dc=local becomes foo/bla or foo/bla/local, depending
1372 * on the value for $base.
1373 *
1374 * If the specified DN contains a basedn which either matches
1375 * the specified base or $config->current['BASE'] it is stripped.
1376 *
1377 * \param string 'dn' the subject for the conversion
1378 * \param string 'base' the base dn, default: $this->config->current['BASE']
1379 * \return a string in the form as described above
1380 */
1381 function convert_department_dn($dn, $base = NULL)
1382 {
1383 global $config;
1385 if($base == NULL){
1386 $base = $config->current['BASE'];
1387 }
1389 /* Build a sub-directory style list of the tree level
1390 specified in $dn */
1391 $dn = preg_replace("/".preg_quote($base, '/')."$/i","",$dn);
1392 if(empty($dn)) return("/");
1395 $dep= "";
1396 foreach (explode(',', $dn) as $rdn){
1397 $dep = preg_replace("/^[^=]+=/","",$rdn)."/".$dep;
1398 }
1400 /* Return and remove accidently trailing slashes */
1401 return(trim($dep, "/"));
1402 }
1405 /*! \brief Return the last sub department part of a '/level1/level2/.../' style value.
1406 *
1407 * Given a DN in the sub-directory style list form, this function returns the
1408 * last sub department part and removes the trailing '/'.
1409 *
1410 * Example:
1411 * \code
1412 * print get_sub_department('local/foo/bar');
1413 * # Prints 'bar'
1414 * print get_sub_department('local/foo/bar/');
1415 * # Also prints 'bar'
1416 * \endcode
1417 *
1418 * \param string 'value' the full department string in sub-directory-style
1419 */
1420 function get_sub_department($value)
1421 {
1422 return (LDAP::fix(preg_replace("%^.*/([^/]+)/?$%", "\\1", $value)));
1423 }
1426 /*! \brief Get the OU of a certain RDN
1427 *
1428 * Given a certain RDN name (ogroupRDN, applicationRDN etc.) this
1429 * function returns either a configured OU or the default
1430 * for the given RDN.
1431 *
1432 * Example:
1433 * \code
1434 * # Determine LDAP base where systems are stored
1435 * $base = get_ou('systemRDN') . $this->config->current['BASE'];
1436 * $ldap->cd($base);
1437 * \endcode
1438 * */
1439 function get_ou($name)
1440 {
1441 global $config;
1443 $map = array(
1444 "roleRDN" => "ou=roles,",
1445 "ogroupRDN" => "ou=groups,",
1446 "applicationRDN" => "ou=apps,",
1447 "systemRDN" => "ou=systems,",
1448 "serverRDN" => "ou=servers,ou=systems,",
1449 "terminalRDN" => "ou=terminals,ou=systems,",
1450 "workstationRDN" => "ou=workstations,ou=systems,",
1451 "printerRDN" => "ou=printers,ou=systems,",
1452 "phoneRDN" => "ou=phones,ou=systems,",
1453 "componentRDN" => "ou=netdevices,ou=systems,",
1454 "sambaMachineAccountRDN" => "ou=winstation,",
1456 "faxBlocklistRDN" => "ou=gofax,ou=systems,",
1457 "systemIncomingRDN" => "ou=incoming,",
1458 "aclRoleRDN" => "ou=aclroles,",
1459 "phoneMacroRDN" => "ou=macros,ou=asterisk,ou=configs,ou=systems,",
1460 "phoneConferenceRDN" => "ou=conferences,ou=asterisk,ou=configs,ou=systems,",
1462 "faiBaseRDN" => "ou=fai,ou=configs,ou=systems,",
1463 "faiScriptRDN" => "ou=scripts,",
1464 "faiHookRDN" => "ou=hooks,",
1465 "faiTemplateRDN" => "ou=templates,",
1466 "faiVariableRDN" => "ou=variables,",
1467 "faiProfileRDN" => "ou=profiles,",
1468 "faiPackageRDN" => "ou=packages,",
1469 "faiPartitionRDN"=> "ou=disk,",
1471 "sudoRDN" => "ou=sudoers,",
1473 "deviceRDN" => "ou=devices,",
1474 "mimetypeRDN" => "ou=mime,");
1476 /* Preset ou... */
1477 if ($config->get_cfg_value($name, "_not_set_") != "_not_set_"){
1478 $ou= $config->get_cfg_value($name);
1479 } elseif (isset($map[$name])) {
1480 $ou = $map[$name];
1481 return($ou);
1482 } else {
1483 trigger_error("No department mapping found for type ".$name);
1484 return "";
1485 }
1487 if ($ou != ""){
1488 if (!preg_match('/^[^=]+=[^=]+/', $ou)){
1489 $ou = @LDAP::convert("ou=$ou");
1490 } else {
1491 $ou = @LDAP::convert("$ou");
1492 }
1494 if(preg_match("/".preg_quote($config->current['BASE'], '/')."$/",$ou)){
1495 return($ou);
1496 }else{
1497 return("$ou,");
1498 }
1500 } else {
1501 return "";
1502 }
1503 }
1506 /*! \brief Get the OU for users
1507 *
1508 * Frontend for get_ou() with userRDN
1509 * */
1510 function get_people_ou()
1511 {
1512 return (get_ou("userRDN"));
1513 }
1516 /*! \brief Get the OU for groups
1517 *
1518 * Frontend for get_ou() with groupRDN
1519 */
1520 function get_groups_ou()
1521 {
1522 return (get_ou("groupRDN"));
1523 }
1526 /*! \brief Get the OU for winstations
1527 *
1528 * Frontend for get_ou() with sambaMachineAccountRDN
1529 */
1530 function get_winstations_ou()
1531 {
1532 return (get_ou("sambaMachineAccountRDN"));
1533 }
1536 /*! \brief Return a base from a given user DN
1537 *
1538 * \code
1539 * get_base_from_people('cn=Max Muster,dc=local')
1540 * # Result is 'dc=local'
1541 * \endcode
1542 *
1543 * \param string 'dn' a DN
1544 * */
1545 function get_base_from_people($dn)
1546 {
1547 global $config;
1549 $pattern= "/^[^,]+,".preg_quote(get_people_ou(), '/')."/i";
1550 $base= preg_replace($pattern, '', $dn);
1552 /* Set to base, if we're not on a correct subtree */
1553 if (!isset($config->idepartments[$base])){
1554 $base= $config->current['BASE'];
1555 }
1557 return ($base);
1558 }
1561 /*! \brief Check if strict naming rules are configured
1562 *
1563 * Return TRUE or FALSE depending on weither strictNamingRules
1564 * are configured or not.
1565 *
1566 * \return Returns TRUE if strictNamingRules is set to true or if the
1567 * config object is not available, otherwise FALSE.
1568 */
1569 function strict_uid_mode()
1570 {
1571 global $config;
1573 if (isset($config)){
1574 return ($config->get_cfg_value("strictNamingRules") == "true");
1575 }
1576 return (TRUE);
1577 }
1580 /*! \brief Get regular expression for checking uids based on the naming
1581 * rules.
1582 * \return string Returns the desired regular expression
1583 */
1584 function get_uid_regexp()
1585 {
1586 /* STRICT adds spaces and case insenstivity to the uid check.
1587 This is dangerous and should not be used. */
1588 if (strict_uid_mode()){
1589 return "^[a-z0-9_-]+$";
1590 } else {
1591 return "^[a-zA-Z0-9 _.-]+$";
1592 }
1593 }
1596 /*! \brief Generate a lock message
1597 *
1598 * This message shows a warning to the user, that a certain object is locked
1599 * and presents some choices how the user can proceed. By default this
1600 * is 'Cancel' or 'Edit anyway', but depending on the function call
1601 * its possible to allow readonly access, too.
1602 *
1603 * Example usage:
1604 * \code
1605 * if (($user = get_lock($this->dn)) != "") {
1606 * return(gen_locked_message($user, $this->dn, TRUE));
1607 * }
1608 * \endcode
1609 *
1610 * \param string 'user' the user who holds the lock
1611 * \param string 'dn' the locked DN
1612 * \param boolean 'allow_readonly' TRUE if readonly access should be permitted,
1613 * FALSE if not (default).
1614 *
1615 *
1616 */
1617 function gen_locked_message($user, $dn, $allow_readonly = FALSE)
1618 {
1619 global $plug, $config;
1621 session::set('dn', $dn);
1622 $remove= false;
1624 /* Save variables from LOCK_VARS_TO_USE in session - for further editing */
1625 if( session::is_set('LOCK_VARS_TO_USE') && count(session::get('LOCK_VARS_TO_USE'))){
1627 $LOCK_VARS_USED_GET = array();
1628 $LOCK_VARS_USED_POST = array();
1629 $LOCK_VARS_USED_REQUEST = array();
1630 $LOCK_VARS_TO_USE = session::get('LOCK_VARS_TO_USE');
1632 foreach($LOCK_VARS_TO_USE as $name){
1634 if(empty($name)){
1635 continue;
1636 }
1638 foreach($_POST as $Pname => $Pvalue){
1639 if(preg_match($name,$Pname)){
1640 $LOCK_VARS_USED_POST[$Pname] = $_POST[$Pname];
1641 }
1642 }
1644 foreach($_GET as $Pname => $Pvalue){
1645 if(preg_match($name,$Pname)){
1646 $LOCK_VARS_USED_GET[$Pname] = $_GET[$Pname];
1647 }
1648 }
1650 foreach($_REQUEST as $Pname => $Pvalue){
1651 if(preg_match($name,$Pname)){
1652 $LOCK_VARS_USED_REQUEST[$Pname] = $_REQUEST[$Pname];
1653 }
1654 }
1655 }
1656 session::set('LOCK_VARS_TO_USE',array());
1657 session::set('LOCK_VARS_USED_GET' , $LOCK_VARS_USED_GET);
1658 session::set('LOCK_VARS_USED_POST' , $LOCK_VARS_USED_POST);
1659 session::set('LOCK_VARS_USED_REQUEST' , $LOCK_VARS_USED_REQUEST);
1660 }
1662 /* Prepare and show template */
1663 $smarty= get_smarty();
1664 $smarty->assign("allow_readonly",$allow_readonly);
1665 if(is_array($dn)){
1666 $msg = "<pre>";
1667 foreach($dn as $sub_dn){
1668 $msg .= "\n".$sub_dn.", ";
1669 }
1670 $msg = preg_replace("/, $/","</pre>",$msg);
1671 }else{
1672 $msg = $dn;
1673 }
1675 $smarty->assign ("dn", $msg);
1676 if ($remove){
1677 $smarty->assign ("action", _("Continue anyway"));
1678 } else {
1679 $smarty->assign ("action", _("Edit anyway"));
1680 }
1681 $smarty->assign ("message", sprintf(_("You're going to edit the LDAP entry/entries %s"), "<b>".$msg."</b>", ""));
1683 return ($smarty->fetch (get_template_path('islocked.tpl')));
1684 }
1687 /*! \brief Return a string/HTML representation of an array
1688 *
1689 * This returns a string representation of a given value.
1690 * It can be used to dump arrays, where every value is printed
1691 * on its own line. The output is targetted at HTML output, it uses
1692 * '<br>' for line breaks. If the value is already a string its
1693 * returned unchanged.
1694 *
1695 * \param mixed 'value' Whatever needs to be printed.
1696 * \return string
1697 */
1698 function to_string ($value)
1699 {
1700 /* If this is an array, generate a text blob */
1701 if (is_array($value)){
1702 $ret= "";
1703 foreach ($value as $line){
1704 $ret.= $line."<br>\n";
1705 }
1706 return ($ret);
1707 } else {
1708 return ($value);
1709 }
1710 }
1713 /*! \brief Return a list of all printers in the current base
1714 *
1715 * Returns an array with the CNs of all printers (objects with
1716 * objectClass gotoPrinter) in the current base.
1717 * ($config->current['BASE']).
1718 *
1719 * Example:
1720 * \code
1721 * $this->printerList = get_printer_list();
1722 * \endcode
1723 *
1724 * \return array an array with the CNs of the printers as key and value.
1725 * */
1726 function get_printer_list()
1727 {
1728 global $config;
1729 $res = array();
1730 $data = get_list('(objectClass=gotoPrinter)',"printer",$config->current['BASE'], array('cn'), GL_SUBSEARCH);
1731 foreach($data as $attrs ){
1732 $res[$attrs['cn'][0]] = $attrs['cn'][0];
1733 }
1734 return $res;
1735 }
1738 /*! \brief Function to rewrite some problematic characters
1739 *
1740 * This function takes a string and replaces all possibly characters in it
1741 * with less problematic characters, as defined in $REWRITE.
1742 *
1743 * \param string 's' the string to rewrite
1744 * \return string 's' the result of the rewrite
1745 * */
1746 function rewrite($s)
1747 {
1748 global $REWRITE;
1750 foreach ($REWRITE as $key => $val){
1751 $s= str_replace("$key", "$val", $s);
1752 }
1754 return ($s);
1755 }
1758 /*! \brief Return the base of a given DN
1759 *
1760 * \param string 'dn' a DN
1761 * */
1762 function dn2base($dn)
1763 {
1764 global $config;
1766 if (get_people_ou() != ""){
1767 $dn= preg_replace('/,'.get_people_ou().'/i' , ',', $dn);
1768 }
1769 if (get_groups_ou() != ""){
1770 $dn= preg_replace('/,'.get_groups_ou().'/i' , ',', $dn);
1771 }
1772 $base= preg_replace ('/^[^,]+,/i', '', $dn);
1774 return ($base);
1775 }
1778 /*! \brief Check if a given command exists and is executable
1779 *
1780 * Test if a given cmdline contains an executable command. Strips
1781 * arguments from the given cmdline.
1782 *
1783 * \param string 'cmdline' the cmdline to check
1784 * \return TRUE if command exists and is executable, otherwise FALSE.
1785 * */
1786 function check_command($cmdline)
1787 {
1788 $cmd= preg_replace("/ .*$/", "", $cmdline);
1790 /* Check if command exists in filesystem */
1791 if (!file_exists($cmd)){
1792 return (FALSE);
1793 }
1795 /* Check if command is executable */
1796 if (!is_executable($cmd)){
1797 return (FALSE);
1798 }
1800 return (TRUE);
1801 }
1804 /*! \brief Print plugin HTML header
1805 *
1806 * \param string 'image' the path of the image to be used next to the headline
1807 * \param string 'image' the headline
1808 * \param string 'info' additional information to print
1809 */
1810 function print_header($image, $headline, $info= "")
1811 {
1812 $display= "<div class=\"plugtop\">\n";
1813 $display.= " <p class=\"center\" style=\"margin:0px 0px 0px 5px;padding:0px;font-size:24px;\"><img class=\"center\" src=\"$image\" align=\"middle\" alt=\"*\"> $headline</p>\n";
1814 $display.= "</div>\n";
1816 if ($info != ""){
1817 $display.= "<div class=\"pluginfo\">\n";
1818 $display.= "$info";
1819 $display.= "</div>\n";
1820 } else {
1821 $display.= "<div style=\"height:5px;\">\n";
1822 $display.= " ";
1823 $display.= "</div>\n";
1824 }
1825 return ($display);
1826 }
1829 /*! \brief Print page number selector for paged lists
1830 *
1831 * \param int 'dcnt' Number of entries
1832 * \param int 'start' Page to start
1833 * \param int 'range' Number of entries per page
1834 * \param string 'post_var' POST variable to check for range
1835 */
1836 function range_selector($dcnt,$start,$range=25,$post_var=false)
1837 {
1839 /* Entries shown left and right from the selected entry */
1840 $max_entries= 10;
1842 /* Initialize and take care that max_entries is even */
1843 $output="";
1844 if ($max_entries & 1){
1845 $max_entries++;
1846 }
1848 if((!empty($post_var))&&(isset($_POST[$post_var]))){
1849 $range= $_POST[$post_var];
1850 }
1852 /* Prevent output to start or end out of range */
1853 if ($start < 0 ){
1854 $start= 0 ;
1855 }
1856 if ($start >= $dcnt){
1857 $start= $range * (int)(($dcnt / $range) + 0.5);
1858 }
1860 $numpages= (($dcnt / $range));
1861 if(((int)($numpages))!=($numpages)){
1862 $numpages = (int)$numpages + 1;
1863 }
1864 if ((((int)$numpages) <= 1 )&&(!$post_var)){
1865 return ("");
1866 }
1867 $ppage= (int)(($start / $range) + 0.5);
1870 /* Align selected page to +/- max_entries/2 */
1871 $begin= $ppage - $max_entries/2;
1872 $end= $ppage + $max_entries/2;
1874 /* Adjust begin/end, so that the selected value is somewhere in
1875 the middle and the size is max_entries if possible */
1876 if ($begin < 0){
1877 $end-= $begin + 1;
1878 $begin= 0;
1879 }
1880 if ($end > $numpages) {
1881 $end= $numpages;
1882 }
1883 if (($end - $begin) < $max_entries && ($end - $max_entries) > 0){
1884 $begin= $end - $max_entries;
1885 }
1887 if($post_var){
1888 $output.= "<div style='border:1px solid #E0E0E0; background-color:#FFFFFF;'>
1889 <table summary='' width='100%'><tr><td style='width:25%'></td><td style='text-align:center;'>";
1890 }else{
1891 $output.= "<div style='border:1px solid #E0E0E0; background-color:#FFFFFF;'>";
1892 }
1894 /* Draw decrement */
1895 if ($start > 0 ) {
1896 $output.=" <a href= \"main.php?plug=".validate($_GET['plug'])."&start=".
1897 (($start-$range))."\">".
1898 "<img class=\"center\" alt=\"\" src=\"images/back.png\" border=0 align=\"middle\"></a>";
1899 }
1901 /* Draw pages */
1902 for ($i= $begin; $i < $end; $i++) {
1903 if ($ppage == $i){
1904 $output.= "<a style=\"vertical-align:middle;background-color:#D0D0D0;\" href=\"main.php?plug=".
1905 validate($_GET['plug'])."&start=".
1906 ($i*$range)."\"> ".($i+1)." </a>";
1907 } else {
1908 $output.= "<a style=\"vertical-align:middle;\" href=\"main.php?plug=".validate($_GET['plug']).
1909 "&start=".($i*$range)."\"> ".($i+1)." </a>";
1910 }
1911 }
1913 /* Draw increment */
1914 if($start < ($dcnt-$range)) {
1915 $output.=" <a href= \"main.php?plug=".validate($_GET['plug'])."&start=".
1916 (($start+($range)))."\">".
1917 "<img class=\"center\" alt=\"\" src=\"images/forward.png\" border=\"0\" align=\"middle\"></a>";
1918 }
1920 if(($post_var)&&($numpages)){
1921 $output.= "</td><td style='width:25%;text-align:right;vertical-align:middle;'> "._("Entries per page")." <select style='vertical-align:middle;' name='".$post_var."' onChange='javascript:document.mainform.submit()'>";
1922 foreach(array(20,50,100,200,"all") as $num){
1923 if($num == "all"){
1924 $var = 10000;
1925 }else{
1926 $var = $num;
1927 }
1928 if($var == $range){
1929 $output.="\n<option selected='selected' value='".$var."'>".$num."</option>";
1930 }else{
1931 $output.="\n<option value='".$var."'>".$num."</option>";
1932 }
1933 }
1934 $output.= "</select></td></tr></table></div>";
1935 }else{
1936 $output.= "</div>";
1937 }
1939 return($output);
1940 }
1943 /*! \brief Generate HTML for the 'Apply filter' button */
1944 function apply_filter()
1945 {
1946 $apply= "";
1948 $apply= ''.
1949 '<table summary="" width="100%" style="background:#EEEEEE;border-top:1px solid #B0B0B0;"><tr><td width="100%" align="right">'.
1950 '<input type="submit" name="apply" value="'._("Apply filter").'"></td></tr></table>';
1952 return ($apply);
1953 }
1956 /*! \brief Generate HTML for the 'Back' button */
1957 function back_to_main()
1958 {
1959 $string= '<br><p class="plugbottom"><input type=submit name="password_back" value="'.
1960 msgPool::backButton().'"></p><input type="hidden" name="ignore">';
1962 return ($string);
1963 }
1966 /*! \brief Put netmask in n.n.n.n format
1967 * \param string 'netmask' The netmask
1968 * \return string Converted netmask
1969 */
1970 function normalize_netmask($netmask)
1971 {
1972 /* Check for notation of netmask */
1973 if (!preg_match('/^([0-9]+\.){3}[0-9]+$/', $netmask)){
1974 $num= (int)($netmask);
1975 $netmask= "";
1977 for ($byte= 0; $byte<4; $byte++){
1978 $result=0;
1980 for ($i= 7; $i>=0; $i--){
1981 if ($num-- > 0){
1982 $result+= pow(2,$i);
1983 }
1984 }
1986 $netmask.= $result.".";
1987 }
1989 return (preg_replace('/\.$/', '', $netmask));
1990 }
1992 return ($netmask);
1993 }
1996 /*! \brief Return the number of set bits in the netmask
1997 *
1998 * For a given subnetmask (for example 255.255.255.0) this returns
1999 * the number of set bits.
2000 *
2001 * Example:
2002 * \code
2003 * $bits = netmask_to_bits('255.255.255.0') # Returns 24
2004 * $bits = netmask_to_bits('255.255.254.0') # Returns 23
2005 * \endcode
2006 *
2007 * Be aware of the fact that the function does not check
2008 * if the given subnet mask is actually valid. For example:
2009 * Bad examples:
2010 * \code
2011 * $bits = netmask_to_bits('255.0.0.255') # Returns 16
2012 * $bits = netmask_to_bits('255.255.0.255') # Returns 24
2013 * \endcode
2014 */
2015 function netmask_to_bits($netmask)
2016 {
2017 list($nm0, $nm1, $nm2, $nm3)= explode('.', $netmask);
2018 $res= 0;
2020 for ($n= 0; $n<4; $n++){
2021 $start= 255;
2022 $name= "nm$n";
2024 for ($i= 0; $i<8; $i++){
2025 if ($start == (int)($$name)){
2026 $res+= 8 - $i;
2027 break;
2028 }
2029 $start-= pow(2,$i);
2030 }
2031 }
2033 return ($res);
2034 }
2037 /*! \brief Recursion helper for gen_id() */
2038 function recurse($rule, $variables)
2039 {
2040 $result= array();
2042 if (!count($variables)){
2043 return array($rule);
2044 }
2046 reset($variables);
2047 $key= key($variables);
2048 $val= current($variables);
2049 unset ($variables[$key]);
2051 foreach($val as $possibility){
2052 $nrule= str_replace("{$key}", $possibility, $rule);
2053 $result= array_merge($result, recurse($nrule, $variables));
2054 }
2056 return ($result);
2057 }
2060 /*! \brief Expands user ID based on possible rules
2061 *
2062 * Unroll given rule string by filling in attributes.
2063 *
2064 * \param string 'rule' The rule string from gosa.conf.
2065 * \param array 'attributes' A dictionary of attribute/value mappings
2066 * \return string Expanded string, still containing the id keyword.
2067 */
2068 function expand_id($rule, $attributes)
2069 {
2070 /* Check for id rule */
2071 if(preg_match('/^id(:|#|!)\d+$/',$rule)){
2072 return (array("{$rule}"));
2073 }
2075 /* Check for clean attribute */
2076 if (preg_match('/^%[a-zA-Z0-9]+$/', $rule)){
2077 $rule= preg_replace('/^%/', '', $rule);
2078 $val= rewrite(str_replace(' ', '', strtolower($attributes[$rule])));
2079 return (array($val));
2080 }
2082 /* Check for attribute with parameters */
2083 if (preg_match('/^%[a-zA-Z0-9]+\[[0-9-]+\]$/', $rule)){
2084 $param= preg_replace('/^[^[]+\[([^]]+)]$/', '\\1', $rule);
2085 $part= preg_replace('/^%/', '', preg_replace('/\[.*$/', '', $rule));
2086 $val= rewrite(str_replace(' ', '', strtolower($attributes[$part])));
2087 $start= preg_replace ('/-.*$/', '', $param);
2088 $stop = preg_replace ('/^[^-]+-/', '', $param);
2090 /* Assemble results */
2091 $result= array();
2092 for ($i= $start; $i<= $stop; $i++){
2093 $result[]= substr($val, 0, $i);
2094 }
2095 return ($result);
2096 }
2098 echo "Error in idGenerator string: don't know how to handle rule $rule.\n";
2099 return (array($rule));
2100 }
2103 /*! \brief Generate a list of uid proposals based on a rule
2104 *
2105 * Unroll given rule string by filling in attributes and replacing
2106 * all keywords.
2107 *
2108 * \param string 'rule' The rule string from gosa.conf.
2109 * \param array 'attributes' A dictionary of attribute/value mappings
2110 * \return array List of valid not used uids
2111 */
2112 function gen_uids($rule, $attributes)
2113 {
2114 global $config;
2116 // Strip out non ascii chars
2117 foreach($attributes as $name => $value){
2118 $value = iconv('UTF-8', 'US-ASCII//TRANSLIT', $value);
2119 $value = preg_replace('/[^(\x20-\x7F)]*/','',$value);
2120 $attributes[$name] = $value;
2121 }
2123 /* Search for keys and fill the variables array with all
2124 possible values for that key. */
2125 $part= "";
2126 $trigger= false;
2127 $stripped= "";
2128 $variables= array();
2130 for ($pos= 0, $l= strlen($rule); $pos < $l; $pos++){
2132 if ($rule[$pos] == "{" ){
2133 $trigger= true;
2134 $part= "";
2135 continue;
2136 }
2138 if ($rule[$pos] == "}" ){
2139 $variables[$pos]= expand_id($part, $attributes);
2140 $stripped.= "{".$pos."}";
2141 $trigger= false;
2142 continue;
2143 }
2145 if ($trigger){
2146 $part.= $rule[$pos];
2147 } else {
2148 $stripped.= $rule[$pos];
2149 }
2150 }
2152 /* Recurse through all possible combinations */
2153 $proposed= recurse($stripped, $variables);
2155 /* Get list of used ID's */
2156 $ldap= $config->get_ldap_link();
2157 $ldap->cd($config->current['BASE']);
2159 /* Remove used uids and watch out for id tags */
2160 $ret= array();
2161 foreach($proposed as $uid){
2163 /* Check for id tag and modify uid if needed */
2164 if(preg_match('/\{id(:|!)\d+}/',$uid, $m)){
2165 $size= preg_replace('/^.*{id(:|!)(\d+)}.*$/', '\\2', $uid);
2167 $start= $m[1]==":"?0:-1;
2168 for ($i= $start, $p= pow(10,$size)-1; $i < $p; $i++){
2169 if ($i == -1) {
2170 $number= "";
2171 } else {
2172 $number= sprintf("%0".$size."d", $i+1);
2173 }
2174 $res= preg_replace('/{id(:|!)\d+}/', $number, $uid);
2176 $ldap->search("(uid=".preg_replace('/[{}]/', '', $res).")",array('dn'));
2177 if($ldap->count() == 0){
2178 $uid= $res;
2179 break;
2180 }
2181 }
2183 /* Remove link if nothing has been found */
2184 $uid= preg_replace('/{id(:|!)\d+}/', '', $uid);
2185 }
2187 if(preg_match('/\{id#\d+}/',$uid)){
2188 $size= preg_replace('/^.*{id#(\d+)}.*$/', '\\1', $uid);
2190 while (true){
2191 mt_srand((double) microtime()*1000000);
2192 $number= sprintf("%0".$size."d", mt_rand(0, pow(10, $size)-1));
2193 $res= preg_replace('/{id#(\d+)}/', $number, $uid);
2194 $ldap->search("(uid=".preg_replace('/[{}]/', '', $res).")",array('dn'));
2195 if($ldap->count() == 0){
2196 $uid= $res;
2197 break;
2198 }
2199 }
2201 /* Remove link if nothing has been found */
2202 $uid= preg_replace('/{id#\d+}/', '', $uid);
2203 }
2205 /* Don't assign used ones */
2206 $ldap->search("(uid=".preg_replace('/[{}]/', '', $uid).")",array('dn'));
2207 if($ldap->count() == 0){
2208 /* Add uid, but remove {} first. These are invalid anyway. */
2209 $ret[]= preg_replace('/[{}]/', '', $uid);
2210 }
2211 }
2213 return(array_unique($ret));
2214 }
2217 /*! \brief Convert various data sizes to bytes
2218 *
2219 * Given a certain value in the format n(g|m|k), where n
2220 * is a value and (g|m|k) stands for Gigabyte, Megabyte and Kilobyte
2221 * this function returns the byte value.
2222 *
2223 * \param string 'value' a value in the above specified format
2224 * \return a byte value or the original value if specified string is simply
2225 * a numeric value
2226 *
2227 */
2228 function to_byte($value) {
2229 $value= strtolower(trim($value));
2231 if(!is_numeric(substr($value, -1))) {
2233 switch(substr($value, -1)) {
2234 case 'g':
2235 $mult= 1073741824;
2236 break;
2237 case 'm':
2238 $mult= 1048576;
2239 break;
2240 case 'k':
2241 $mult= 1024;
2242 break;
2243 }
2245 return ($mult * (int)substr($value, 0, -1));
2246 } else {
2247 return $value;
2248 }
2249 }
2252 /*! \brief Check if a value exists in an array (case-insensitive)
2253 *
2254 * This is just as http://php.net/in_array except that the comparison
2255 * is case-insensitive.
2256 *
2257 * \param string 'value' needle
2258 * \param array 'items' haystack
2259 */
2260 function in_array_ics($value, $items)
2261 {
2262 return preg_grep('/^'.preg_quote($value, '/').'$/i', $items);
2263 }
2266 /*! \brief Generate a clickable alphabet */
2267 function generate_alphabet($count= 10)
2268 {
2269 $characters= _("*ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
2270 $alphabet= "";
2271 $c= 0;
2273 /* Fill cells with charaters */
2274 for ($i= 0, $l= mb_strlen($characters, 'UTF8'); $i<$l; $i++){
2275 if ($c == 0){
2276 $alphabet.= "<tr>";
2277 }
2279 $ch = mb_substr($characters, $i, 1, "UTF8");
2280 $alphabet.= "<td><a class=\"alphaselect\" href=\"main.php?plug=".
2281 validate($_GET['plug'])."&search=".$ch."\"> ".$ch." </a></td>";
2283 if ($c++ == $count){
2284 $alphabet.= "</tr>";
2285 $c= 0;
2286 }
2287 }
2289 /* Fill remaining cells */
2290 while ($c++ <= $count){
2291 $alphabet.= "<td> </td>";
2292 }
2294 return ($alphabet);
2295 }
2298 /*! \brief Removes malicious characters from a (POST) string. */
2299 function validate($string)
2300 {
2301 return (strip_tags(str_replace('\0', '', $string)));
2302 }
2305 /*! \brief Evaluate the current GOsa version from the build in revision string */
2306 function get_gosa_version()
2307 {
2308 global $svn_revision, $svn_path;
2310 /* Extract informations */
2311 $revision= preg_replace('/^[^0-9]*([0-9]+)[^0-9]*$/', '\1', $svn_revision);
2313 // Extract the relevant part out of the svn url
2314 $release= preg_replace('%^.*/gosa/(.*)/include/functions.inc.*$%', '\1', $svn_path);
2316 // Remove stuff which is not interesting
2317 if(preg_match("/gosa-core/i", $release)) $release = preg_replace("/[\/]gosa-core/i","",$release);
2319 // A Tagged Version
2320 if(preg_match("#/tags/#i", $svn_path)){
2321 $release = preg_replace("/tags[\/]*/i","",$release);
2322 $release = preg_replace("/\//","",$release) ;
2323 return (sprintf(_("GOsa %s"),$release));
2324 }
2326 // A Branched Version
2327 if(preg_match("#/branches/#i", $svn_path)){
2328 $release = preg_replace("/branches[\/]*/i","",$release);
2329 $release = preg_replace("/\//","",$release) ;
2330 return (sprintf(_("GOsa %s snapshot (Rev %s)"),$release , $revision));
2331 }
2333 // The trunk version
2334 if(preg_match("#/trunk/#i", $svn_path)){
2335 return (sprintf(_("GOsa development snapshot (Rev %s)"), $revision));
2336 }
2338 return (sprintf(_("GOsa $release"), $revision));
2339 }
2342 /*! \brief Recursively delete a path in the file system
2343 *
2344 * Will delete the given path and all its files recursively.
2345 * Can also follow links if told so.
2346 *
2347 * \param string 'path'
2348 * \param boolean 'followLinks' TRUE to follow links, FALSE (default)
2349 * for not following links
2350 */
2351 function rmdirRecursive($path, $followLinks=false) {
2352 $dir= opendir($path);
2353 while($entry= readdir($dir)) {
2354 if(is_file($path."/".$entry) || ((!$followLinks) && is_link($path."/".$entry))) {
2355 unlink($path."/".$entry);
2356 } elseif (is_dir($path."/".$entry) && $entry!='.' && $entry!='..') {
2357 rmdirRecursive($path."/".$entry);
2358 }
2359 }
2360 closedir($dir);
2361 return rmdir($path);
2362 }
2365 /*! \brief Get directory content information
2366 *
2367 * Returns the content of a directory as an array in an
2368 * ascended sorted manner.
2369 *
2370 * \param string 'path'
2371 * \param boolean weither to sort the content descending.
2372 */
2373 function scan_directory($path,$sort_desc=false)
2374 {
2375 $ret = false;
2377 /* is this a dir ? */
2378 if(is_dir($path)) {
2380 /* is this path a readable one */
2381 if(is_readable($path)){
2383 /* Get contents and write it into an array */
2384 $ret = array();
2386 $dir = opendir($path);
2388 /* Is this a correct result ?*/
2389 if($dir){
2390 while($fp = readdir($dir))
2391 $ret[]= $fp;
2392 }
2393 }
2394 }
2395 /* Sort array ascending , like scandir */
2396 sort($ret);
2398 /* Sort descending if parameter is sort_desc is set */
2399 if($sort_desc) {
2400 $ret = array_reverse($ret);
2401 }
2403 return($ret);
2404 }
2407 /*! \brief Clean the smarty compile dir */
2408 function clean_smarty_compile_dir($directory)
2409 {
2410 global $svn_revision;
2412 if(is_dir($directory) && is_readable($directory)) {
2413 // Set revision filename to REVISION
2414 $revision_file= $directory."/REVISION";
2416 /* Is there a stamp containing the current revision? */
2417 if(!file_exists($revision_file)) {
2418 // create revision file
2419 create_revision($revision_file, $svn_revision);
2420 } else {
2421 # check for "$config->...['CONFIG']/revision" and the
2422 # contents should match the revision number
2423 if(!compare_revision($revision_file, $svn_revision)){
2424 // If revision differs, clean compile directory
2425 foreach(scan_directory($directory) as $file) {
2426 if(($file==".")||($file=="..")) continue;
2427 if( is_file($directory."/".$file) &&
2428 is_writable($directory."/".$file)) {
2429 // delete file
2430 if(!unlink($directory."/".$file)) {
2431 msg_dialog::display(_("Internal error"), sprintf(_("File '%s' could not be deleted."), $directory."/".$file), ERROR_DIALOG);
2432 // This should never be reached
2433 }
2434 }
2435 }
2436 // We should now create a fresh revision file
2437 clean_smarty_compile_dir($directory);
2438 } else {
2439 // Revision matches, nothing to do
2440 }
2441 }
2442 } else {
2443 // Smarty compile dir is not accessible
2444 // (Smarty will warn about this)
2445 }
2446 }
2449 function create_revision($revision_file, $revision)
2450 {
2451 $result= false;
2453 if(is_dir(dirname($revision_file)) && is_writable(dirname($revision_file))) {
2454 if($fh= fopen($revision_file, "w")) {
2455 if(fwrite($fh, $revision)) {
2456 $result= true;
2457 }
2458 }
2459 fclose($fh);
2460 } else {
2461 msg_dialog::display(_("Internal error"), _("Cannot write to revision file!"), ERROR_DIALOG);
2462 }
2464 return $result;
2465 }
2468 function compare_revision($revision_file, $revision)
2469 {
2470 // false means revision differs
2471 $result= false;
2473 if(file_exists($revision_file) && is_readable($revision_file)) {
2474 // Open file
2475 if($fh= fopen($revision_file, "r")) {
2476 // Compare File contents with current revision
2477 if($revision == fread($fh, filesize($revision_file))) {
2478 $result= true;
2479 }
2480 } else {
2481 msg_dialog::display(_("Internal error"), _("Cannot write to revision file!"), ERROR_DIALOG);
2482 }
2483 // Close file
2484 fclose($fh);
2485 }
2487 return $result;
2488 }
2491 /*! \brief Return HTML for a progressbar
2492 *
2493 * \code
2494 * $smarty->assign("installprogress", progressbar($current_progress_in_percent),100,15,true);
2495 * \endcode
2496 *
2497 * \param int 'percentage' Value to display
2498 * \param int 'width' width of the resulting output
2499 * \param int 'height' height of the resulting output
2500 * \param boolean 'showvalue' weither to show the percentage in the progressbar or not
2501 * */
2502 function progressbar($percentage,$width=100,$height=15,$showvalue=false)
2503 {
2504 return("<img src='progress.php?x=$width&y=$height&p=$percentage'>");
2505 }
2508 /*! \brief Lookup a key in an array case-insensitive
2509 *
2510 * Given an associative array this can lookup the value of
2511 * a certain key, regardless of the case.
2512 *
2513 * \code
2514 * $items = array ('FOO' => 'blub', 'bar' => 'blub');
2515 * array_key_ics('foo', $items); # Returns 'blub'
2516 * array_key_ics('BAR', $items); # Returns 'blub'
2517 * \endcode
2518 *
2519 * \param string 'key' needle
2520 * \param array 'items' haystack
2521 */
2522 function array_key_ics($ikey, $items)
2523 {
2524 $tmp= array_change_key_case($items, CASE_LOWER);
2525 $ikey= strtolower($ikey);
2526 if (isset($tmp[$ikey])){
2527 return($tmp[$ikey]);
2528 }
2530 return ('');
2531 }
2534 /*! \brief Determine if two arrays are different
2535 *
2536 * \param array 'src'
2537 * \param array 'dst'
2538 * \return boolean TRUE or FALSE
2539 * */
2540 function array_differs($src, $dst)
2541 {
2542 /* If the count is differing, the arrays differ */
2543 if (count ($src) != count ($dst)){
2544 return (TRUE);
2545 }
2547 return (count(array_diff($src, $dst)) != 0);
2548 }
2551 function saveFilter($a_filter, $values)
2552 {
2553 if (isset($_POST['regexit'])){
2554 $a_filter["regex"]= $_POST['regexit'];
2556 foreach($values as $type){
2557 if (isset($_POST[$type])) {
2558 $a_filter[$type]= "checked";
2559 } else {
2560 $a_filter[$type]= "";
2561 }
2562 }
2563 }
2565 /* React on alphabet links if needed */
2566 if (isset($_GET['search'])){
2567 $s= mb_substr(validate($_GET['search']), 0, 1, "UTF8")."*";
2568 if ($s == "**"){
2569 $s= "*";
2570 }
2571 $a_filter['regex']= $s;
2572 }
2574 return ($a_filter);
2575 }
2578 /*! \brief Escape all LDAP filter relevant characters */
2579 function normalizeLdap($input)
2580 {
2581 return (addcslashes($input, '()|'));
2582 }
2585 /*! \brief Return the gosa base directory */
2586 function get_base_dir()
2587 {
2588 global $BASE_DIR;
2590 return $BASE_DIR;
2591 }
2594 /*! \brief Test weither we are allowed to read the object */
2595 function obj_is_readable($dn, $object, $attribute)
2596 {
2597 global $ui;
2599 return preg_match('/r/', $ui->get_permissions($dn, $object, $attribute));
2600 }
2603 /*! \brief Test weither we are allowed to change the object */
2604 function obj_is_writable($dn, $object, $attribute)
2605 {
2606 global $ui;
2608 return preg_match('/w/', $ui->get_permissions($dn, $object, $attribute));
2609 }
2612 /*! \brief Explode a DN into its parts
2613 *
2614 * Similar to explode (http://php.net/explode), but a bit more specific
2615 * for the needs when splitting, exploding LDAP DNs.
2616 *
2617 * \param string 'dn' the DN to split
2618 * \param config-object a config object. only neeeded if DN shall be verified in the LDAP
2619 * \param boolean verify_in_ldap check weither DN is valid
2620 *
2621 */
2622 function gosa_ldap_explode_dn($dn,$config = NULL,$verify_in_ldap=false)
2623 {
2624 /* Initialize variables */
2625 $ret = array("count" => 0); // Set count to 0
2626 $next = true; // if false, then skip next loops and return
2627 $cnt = 0; // Current number of loops
2628 $max = 100; // Just for security, prevent looops
2629 $ldap = NULL; // To check if created result a valid
2630 $keep = ""; // save last failed parse string
2632 /* Check each parsed dn in ldap ? */
2633 if($config!==NULL && $verify_in_ldap){
2634 $ldap = $config->get_ldap_link();
2635 }
2637 /* Lets start */
2638 $called = false;
2639 while(preg_match("/,/",$dn) && $next && $cnt < $max){
2641 $cnt ++;
2642 if(!preg_match("/,/",$dn)){
2643 $next = false;
2644 }
2645 $object = preg_replace("/[,].*$/","",$dn);
2646 $dn = preg_replace("/^[^,]+,/","",$dn);
2648 $called = true;
2650 /* Check if current dn is valid */
2651 if($ldap!==NULL){
2652 $ldap->cd($dn);
2653 $ldap->cat($dn,array("dn"));
2654 if($ldap->count()){
2655 $ret[] = $keep.$object;
2656 $keep = "";
2657 }else{
2658 $keep .= $object.",";
2659 }
2660 }else{
2661 $ret[] = $keep.$object;
2662 $keep = "";
2663 }
2664 }
2666 /* No dn was posted */
2667 if($cnt == 0 && !empty($dn)){
2668 $ret[] = $dn;
2669 }
2671 /* Append the rest */
2672 $test = $keep.$dn;
2673 if($called && !empty($test)){
2674 $ret[] = $keep.$dn;
2675 }
2676 $ret['count'] = count($ret) - 1;
2678 return($ret);
2679 }
2682 function get_base_from_hook($dn, $attrib)
2683 {
2684 global $config;
2686 if ($config->get_cfg_value("baseIdHook") != ""){
2688 /* Call hook script - if present */
2689 $command= $config->get_cfg_value("baseIdHook");
2691 if ($command != ""){
2692 $command.= " ".escapeshellarg(LDAP::fix($dn))." ".escapeshellarg($attrib);
2693 if (check_command($command)){
2694 @DEBUG (DEBUG_SHELL, __LINE__, __FUNCTION__, __FILE__, $command, "Execute");
2695 exec($command, $output);
2696 if (preg_match("/^[0-9]+$/", $output[0])){
2697 return ($output[0]);
2698 } else {
2699 msg_dialog::display(_("Warning"), _("'baseIdHook' is not available. Using default base!"), WARNING_DIALOG);
2700 return ($config->get_cfg_value("uidNumberBase"));
2701 }
2702 } else {
2703 msg_dialog::display(_("Warning"), _("'baseIdHook' is not available. Using default base!"), WARNING_DIALOG);
2704 return ($config->get_cfg_value("uidNumberBase"));
2705 }
2707 } else {
2709 msg_dialog::display(_("Warning"), _("'baseIdHook' is not available. Using default base!"), WARNING_DIALOG);
2710 return ($config->get_cfg_value("uidNumberBase"));
2712 }
2713 }
2714 }
2717 /*! \brief Check if schema version matches the requirements */
2718 function check_schema_version($class, $version)
2719 {
2720 return preg_match("/\(v$version\)/", $class['DESC']);
2721 }
2724 /*! \brief Check if LDAP schema matches the requirements */
2725 function check_schema($cfg,$rfc2307bis = FALSE)
2726 {
2727 $messages= array();
2729 /* Get objectclasses */
2730 $ldap = new ldapMultiplexer(new LDAP($cfg['admin'],$cfg['password'],$cfg['connection'] ,FALSE, $cfg['tls']));
2731 $objectclasses = $ldap->get_objectclasses();
2732 if(count($objectclasses) == 0){
2733 msg_dialog::display(_("LDAP warning"), _("Cannot get schema information from server. No schema check possible!"), WARNING_DIALOG);
2734 }
2736 /* This is the default block used for each entry.
2737 * to avoid unset indexes.
2738 */
2739 $def_check = array("REQUIRED_VERSION" => "0",
2740 "SCHEMA_FILES" => array(),
2741 "CLASSES_REQUIRED" => array(),
2742 "STATUS" => FALSE,
2743 "IS_MUST_HAVE" => FALSE,
2744 "MSG" => "",
2745 "INFO" => "");#_("There is currently no information specified for this schema extension."));
2747 /* The gosa base schema */
2748 $checks['gosaObject'] = $def_check;
2749 $checks['gosaObject']['REQUIRED_VERSION'] = "2.6.1";
2750 $checks['gosaObject']['SCHEMA_FILES'] = array("gosa-samba3.schema");
2751 $checks['gosaObject']['CLASSES_REQUIRED'] = array("gosaObject");
2752 $checks['gosaObject']['IS_MUST_HAVE'] = TRUE;
2754 /* GOsa Account class */
2755 $checks["gosaAccount"]["REQUIRED_VERSION"]= "2.6.6";
2756 $checks["gosaAccount"]["SCHEMA_FILES"] = array("gosa-samba3.schema");
2757 $checks["gosaAccount"]["CLASSES_REQUIRED"]= array("gosaAccount");
2758 $checks["gosaAccount"]["IS_MUST_HAVE"] = TRUE;
2759 $checks["gosaAccount"]["INFO"] = _("Used to store account specific informations.");
2761 /* GOsa lock entry, used to mark currently edited objects as 'in use' */
2762 $checks["gosaLockEntry"]["REQUIRED_VERSION"] = "2.6.1";
2763 $checks["gosaLockEntry"]["SCHEMA_FILES"] = array("gosa-samba3.schema");
2764 $checks["gosaLockEntry"]["CLASSES_REQUIRED"] = array("gosaLockEntry");
2765 $checks["gosaLockEntry"]["IS_MUST_HAVE"] = TRUE;
2766 $checks["gosaLockEntry"]["INFO"] = _("Used to lock currently edited entries to avoid multiple changes at the same time.");
2768 /* Some other checks */
2769 foreach(array(
2770 "gosaCacheEntry" => array("version" => "2.6.1", "class" => "gosaAccount"),
2771 "gosaDepartment" => array("version" => "2.6.1", "class" => "gosaAccount"),
2772 "goFaxAccount" => array("version" => "1.0.4", "class" => "gofaxAccount","file" => "gofax.schema"),
2773 "goFaxSBlock" => array("version" => "1.0.4", "class" => "gofaxAccount","file" => "gofax.schema"),
2774 "goFaxRBlock" => array("version" => "1.0.4", "class" => "gofaxAccount","file" => "gofax.schema"),
2775 "gosaUserTemplate" => array("version" => "2.6.1", "class" => "posixAccount","file" => "nis.schema"),
2776 "gosaMailAccount" => array("version" => "2.6.1", "class" => "mailAccount","file" => "gosa-samba3.schema"),
2777 "gosaProxyAccount" => array("version" => "2.6.1", "class" => "proxyAccount","file" => "gosa-samba3.schema"),
2778 "gosaApplication" => array("version" => "2.6.1", "class" => "appgroup","file" => "gosa.schema"),
2779 "gosaApplicationGroup" => array("version" => "2.6.1", "class" => "appgroup","file" => "gosa.schema"),
2780 "GOhard" => array("version" => "2.6.1", "class" => "terminals","file" => "goto.schema"),
2781 "gotoTerminal" => array("version" => "2.6.1", "class" => "terminals","file" => "goto.schema"),
2782 "goServer" => array("version" => "2.6.1", "class" => "server","file" => "goserver.schema"),
2783 "goTerminalServer" => array("version" => "2.6.1", "class" => "terminals","file" => "goto.schema"),
2784 "goShareServer" => array("version" => "2.6.1", "class" => "terminals","file" => "goto.schema"),
2785 "goNtpServer" => array("version" => "2.6.1", "class" => "terminals","file" => "goto.schema"),
2786 "goSyslogServer" => array("version" => "2.6.1", "class" => "terminals","file" => "goto.schema"),
2787 "goLdapServer" => array("version" => "2.6.1", "class" => "goServer"),
2788 "goCupsServer" => array("version" => "2.6.1", "class" => array("posixAccount", "terminals"),),
2789 "goImapServer" => array("version" => "2.6.1", "class" => array("mailAccount", "mailgroup"),"file" => "gosa-samba3.schema"),
2790 "goKrbServer" => array("version" => "2.6.1", "class" => "goServer"),
2791 "goFaxServer" => array("version" => "2.6.1", "class" => "gofaxAccount","file" => "gofax.schema"),
2792 ) as $name => $values){
2794 $checks[$name] = $def_check;
2795 if(isset($values['version'])){
2796 $checks[$name]["REQUIRED_VERSION"] = $values['version'];
2797 }
2798 if(isset($values['file'])){
2799 $checks[$name]["SCHEMA_FILES"] = array($values['file']);
2800 }
2801 if (isset($values['class'])) {
2802 $checks[$name]["CLASSES_REQUIRED"] = is_array($values['class'])?$values['class']:array($values['class']);
2803 }
2804 }
2805 foreach($checks as $name => $value){
2806 foreach($value['CLASSES_REQUIRED'] as $class){
2808 if(!isset($objectclasses[$name])){
2809 if($value['IS_MUST_HAVE']){
2810 $checks[$name]['STATUS'] = FALSE;
2811 $checks[$name]['MSG'] = sprintf(_("Missing required object class '%s'!"),$class);
2812 } else {
2813 $checks[$name]['STATUS'] = TRUE;
2814 $checks[$name]['MSG'] = sprintf(_("Missing optional object class '%s'!"),$class);
2815 }
2816 }elseif(!check_schema_version($objectclasses[$name],$value['REQUIRED_VERSION'])){
2817 $checks[$name]['STATUS'] = FALSE;
2819 $checks[$name]['MSG'] = sprintf(_("Version mismatch for required object class '%s' (!=%s)!"), $class, $value['REQUIRED_VERSION']);
2820 }else{
2821 $checks[$name]['STATUS'] = TRUE;
2822 $checks[$name]['MSG'] = sprintf(_("Class(es) available"));
2823 }
2824 }
2825 }
2827 $tmp = $objectclasses;
2829 /* The gosa base schema */
2830 $checks['posixGroup'] = $def_check;
2831 $checks['posixGroup']['REQUIRED_VERSION'] = "2.6.1";
2832 $checks['posixGroup']['SCHEMA_FILES'] = array("gosa-samba3.schema");
2833 $checks['posixGroup']['CLASSES_REQUIRED'] = array("posixGroup");
2834 $checks['posixGroup']['STATUS'] = TRUE;
2835 $checks['posixGroup']['IS_MUST_HAVE'] = TRUE;
2836 $checks['posixGroup']['MSG'] = "";
2837 $checks['posixGroup']['INFO'] = "";
2839 /* Depending on selected rfc2307bis mode, we need different schema configurations */
2840 if(isset($tmp['posixGroup'])){
2842 if($rfc2307bis && isset($tmp['posixGroup']['STRUCTURAL'])){
2843 $checks['posixGroup']['STATUS'] = FALSE;
2844 $checks['posixGroup']['MSG'] = _("You have enabled the rfc2307bis option on the 'ldap setup' step, but your schema configuration do not support this option.");
2845 $checks['posixGroup']['INFO'] = _("In order to use rfc2307bis conform groups the objectClass 'posixGroup' must be AUXILIARY");
2846 }
2847 if(!$rfc2307bis && !isset($tmp['posixGroup']['STRUCTURAL'])){
2848 $checks['posixGroup']['STATUS'] = FALSE;
2849 $checks['posixGroup']['MSG'] = _("Your schema is configured to support the rfc2307bis group, but you have disabled this option on the 'ldap setup' step.");
2850 $checks['posixGroup']['INFO'] = _("The objectClass 'posixGroup' must be STRUCTURAL");
2851 }
2852 }
2854 return($checks);
2855 }
2858 function get_languages($languages_in_own_language = FALSE,$strip_region_tag = FALSE)
2859 {
2860 $tmp = array(
2861 "de_DE" => "German",
2862 "fr_FR" => "French",
2863 "it_IT" => "Italian",
2864 "es_ES" => "Spanish",
2865 "en_US" => "English",
2866 "nl_NL" => "Dutch",
2867 "pl_PL" => "Polish",
2868 #"sv_SE" => "Swedish",
2869 "zh_CN" => "Chinese",
2870 "vi_VN" => "Vietnamese",
2871 "ru_RU" => "Russian");
2873 $tmp2= array(
2874 "de_DE" => _("German"),
2875 "fr_FR" => _("French"),
2876 "it_IT" => _("Italian"),
2877 "es_ES" => _("Spanish"),
2878 "en_US" => _("English"),
2879 "nl_NL" => _("Dutch"),
2880 "pl_PL" => _("Polish"),
2881 #"sv_SE" => _("Swedish"),
2882 "zh_CN" => _("Chinese"),
2883 "vi_VN" => _("Vietnamese"),
2884 "ru_RU" => _("Russian"));
2886 $ret = array();
2887 if($languages_in_own_language){
2889 $old_lang = setlocale(LC_ALL, 0);
2891 /* If the locale wasn't correclty set before, there may be an incorrect
2892 locale returned. Something like this:
2893 C_CTYPE=de_DE.UTF-8;LC_NUMERIC=C;LC_TIME=de_DE.UTF-8;LC ...
2894 Extract the locale name from this string and use it to restore old locale.
2895 */
2896 if(preg_match("/LC_CTYPE/",$old_lang)){
2897 $old_lang = preg_replace("/^.*LC_CTYPE=([^;]*).*$/","\\1",$old_lang);
2898 }
2900 foreach($tmp as $key => $name){
2901 $lang = $key.".UTF-8";
2902 setlocale(LC_ALL, $lang);
2903 if($strip_region_tag){
2904 $ret[preg_replace("/^([^_]*).*$/","\\1",$key)] = _($name)." (".$tmp2[$key].")";
2905 }else{
2906 $ret[$key] = _($name)." (".$tmp2[$key].")";
2907 }
2908 }
2909 setlocale(LC_ALL, $old_lang);
2910 }else{
2911 foreach($tmp as $key => $name){
2912 if($strip_region_tag){
2913 $ret[preg_replace("/^([^_]*).*/","\\1",$key)] = _($name);
2914 }else{
2915 $ret[$key] = _($name);
2916 }
2917 }
2918 }
2919 return($ret);
2920 }
2923 /*! \brief Returns contents of the given POST variable and check magic quotes settings
2924 *
2925 * Depending on the magic quotes settings this returns a stripclashed'ed version of
2926 * a certain POST variable.
2927 *
2928 * \param string 'name' the POST var to return ($_POST[$name])
2929 * \return string
2930 * */
2931 function get_post($name)
2932 {
2933 if(!isset($_POST[$name])){
2934 trigger_error("Requested POST value (".$name.") does not exists, you should add a check to prevent this message.");
2935 return(FALSE);
2936 }
2938 if(get_magic_quotes_gpc()){
2939 return(stripcslashes(validate($_POST[$name])));
2940 }else{
2941 return(validate($_POST[$name]));
2942 }
2943 }
2946 /*! \brief Return class name in correct case */
2947 function get_correct_class_name($cls)
2948 {
2949 global $class_mapping;
2950 if(isset($class_mapping) && is_array($class_mapping)){
2951 foreach($class_mapping as $class => $file){
2952 if(preg_match("/^".$cls."$/i",$class)){
2953 return($class);
2954 }
2955 }
2956 }
2957 return(FALSE);
2958 }
2961 /*! \brief Change the password of a given DN
2962 *
2963 * Change the password of a given DN with the specified hash.
2964 *
2965 * \param string 'dn' the DN whose password shall be changed
2966 * \param string 'password' the password
2967 * \param int mode
2968 * \param string 'hash' which hash to use to encrypt it, default is empty
2969 * for cleartext storage.
2970 * \return boolean TRUE on success FALSE on error
2971 */
2972 function change_password ($dn, $password, $mode=0, $hash= "")
2973 {
2974 global $config;
2975 $newpass= "";
2977 /* Convert to lower. Methods are lowercase */
2978 $hash= strtolower($hash);
2980 // Get all available encryption Methods
2982 // NON STATIC CALL :)
2983 $methods = new passwordMethod(session::get('config'),$dn);
2984 $available = $methods->get_available_methods();
2986 // read current password entry for $dn, to detect the encryption Method
2987 $ldap = $config->get_ldap_link();
2988 $ldap->cat ($dn, array("shadowLastChange", "userPassword", "uid"));
2989 $attrs = $ldap->fetch ();
2991 /* Is ensure that clear passwords will stay clear */
2992 if($hash == "" && isset($attrs['userPassword'][0]) && !preg_match ("/^{([^}]+)}(.+)/", $attrs['userPassword'][0])){
2993 $hash = "clear";
2994 }
2996 // Detect the encryption Method
2997 if ( (isset($attrs['userPassword'][0]) && preg_match ("/^{([^}]+)}(.+)/", $attrs['userPassword'][0], $matches)) || $hash != ""){
2999 /* Check for supported algorithm */
3000 mt_srand((double) microtime()*1000000);
3002 /* Extract used hash */
3003 if ($hash == ""){
3004 $test = passwordMethod::get_method($attrs['userPassword'][0],$dn);
3005 } else {
3006 $test = new $available[$hash]($config,$dn);
3007 $test->set_hash($hash);
3008 }
3010 } else {
3011 // User MD5 by default
3012 $hash= "md5";
3013 $test = new $available['md5']($config, $dn);
3014 }
3016 if($test instanceOf passwordMethod){
3018 $deactivated = $test->is_locked($config,$dn);
3020 /* Feed password backends with information */
3021 $test->dn= $dn;
3022 $test->attrs= $attrs;
3023 $newpass= $test->generate_hash($password);
3025 // Update shadow timestamp?
3026 if (isset($attrs["shadowLastChange"][0])){
3027 $shadow= (int)(date("U") / 86400);
3028 } else {
3029 $shadow= 0;
3030 }
3032 // Write back modified entry
3033 $ldap->cd($dn);
3034 $attrs= array();
3036 // Not for groups
3037 if ($mode == 0){
3039 // Create SMB Password
3040 if ($config->get_cfg_value('sambaHashHook', NULL)) {
3041 $attrs= generate_smb_nt_hash($password);
3043 if ($shadow != 0){
3044 $attrs['shadowLastChange']= $shadow;
3045 }
3046 }
3047 }
3049 $attrs['userPassword']= array();
3050 $attrs['userPassword']= $newpass;
3052 $ldap->modify($attrs);
3054 /* Read ! if user was deactivated */
3055 if($deactivated){
3056 $test->lock_account($config,$dn);
3057 }
3059 new log("modify","users/passwordMethod",$dn,array_keys($attrs),$ldap->get_error());
3061 if (!$ldap->success()) {
3062 msg_dialog::display(_("LDAP error"), msgPool::ldaperror($ldap->get_error(), $dn, LDAP_MOD, ERROR_DIALOG));
3063 } else {
3065 /* Run backend method for change/create */
3066 if(!$test->set_password($password)){
3067 return(FALSE);
3068 }
3070 /* Find postmodify entries for this class */
3071 $command= $config->search("password", "POSTMODIFY",array('menu'));
3073 if ($command != ""){
3074 /* Walk through attribute list */
3075 $command= preg_replace("/%userPassword/", escapeshellarg($password), $command);
3076 $command= preg_replace("/%dn/", escapeshellarg($dn), $command);
3078 if (check_command($command)){
3079 @DEBUG (DEBUG_SHELL, __LINE__, __FUNCTION__, __FILE__, $command, "Execute");
3080 exec($command);
3081 } else {
3082 $message= sprintf(_("Command '%s', specified as POSTMODIFY for plugin '%s' doesn't seem to exist."), $command, "password");
3083 msg_dialog::display(_("Configuration error"), $message, ERROR_DIALOG);
3084 }
3085 }
3086 }
3087 return(TRUE);
3088 }
3089 }
3092 /*! \brief Generate samba hashes
3093 *
3094 * Given a certain password this constructs an array like
3095 * array['sambaLMPassword'] etc.
3096 *
3097 * \param string 'password'
3098 * \return array contains several keys for lmPassword, ntPassword, pwdLastSet, etc. depending
3099 * on the samba version
3100 */
3101 function generate_smb_nt_hash($password)
3102 {
3103 global $config;
3105 # Try to use gosa-si?
3106 if ($config->get_cfg_value("gosaSupportURI") != ""){
3107 $res= gosaSupportDaemon::send("gosa_gen_smb_hash", "GOSA", array("password" => $password), TRUE);
3108 if (isset($res['XML']['HASH'])){
3109 $hash= $res['XML']['HASH'];
3110 } else {
3111 $hash= "";
3112 }
3114 if ($hash == "") {
3115 msg_dialog::display(_("Configuration error"), _("Cannot generate samba hash!"), ERROR_DIALOG);
3116 return ("");
3117 }
3118 } else {
3119 $tmp= $config->get_cfg_value('sambaHashHook')." ".escapeshellarg($password);
3120 @DEBUG (DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $tmp, "Execute");
3122 exec($tmp, $ar);
3123 flush();
3124 reset($ar);
3125 $hash= current($ar);
3127 if ($hash == "") {
3128 msg_dialog::display(_("Configuration error"), sprintf(_("Cannot generate samba hash: running '%s' failed, check the 'sambaHashHook'!"),$config->get_cfg_value('sambaHashHook')), ERROR_DIALOG);
3129 return ("");
3130 }
3131 }
3133 list($lm,$nt)= explode(":", trim($hash));
3135 $attrs['sambaLMPassword']= $lm;
3136 $attrs['sambaNTPassword']= $nt;
3137 $attrs['sambaPwdLastSet']= date('U');
3138 $attrs['sambaBadPasswordCount']= "0";
3139 $attrs['sambaBadPasswordTime']= "0";
3140 return($attrs);
3141 }
3144 /*! \brief Get the Change Sequence Number of a certain DN
3145 *
3146 * To verify if a given object has been changed outside of Gosa
3147 * in the meanwhile, this function can be used to get the entryCSN
3148 * from the LDAP directory. It uses the attribute as configured
3149 * in modificationDetectionAttribute
3150 *
3151 * \param string 'dn'
3152 * \return either the result or "" in any other case
3153 */
3154 function getEntryCSN($dn)
3155 {
3156 global $config;
3157 if(empty($dn) || !is_object($config)){
3158 return("");
3159 }
3161 /* Get attribute that we should use as serial number */
3162 $attr= $config->get_cfg_value("modificationDetectionAttribute");
3163 if($attr != ""){
3164 $ldap = $config->get_ldap_link();
3165 $ldap->cat($dn,array($attr));
3166 $csn = $ldap->fetch();
3167 if(isset($csn[$attr][0])){
3168 return($csn[$attr][0]);
3169 }
3170 }
3171 return("");
3172 }
3175 /*! \brief Add (a) given objectClass(es) to an attrs entry
3176 *
3177 * The function adds the specified objectClass(es) to the given
3178 * attrs entry.
3179 *
3180 * \param mixed 'classes' Either a single objectClass or several objectClasses
3181 * as an array
3182 * \param array 'attrs' The attrs array to be modified.
3183 *
3184 * */
3185 function add_objectClass($classes, &$attrs)
3186 {
3187 if (is_array($classes)){
3188 $list= $classes;
3189 } else {
3190 $list= array($classes);
3191 }
3193 foreach ($list as $class){
3194 $attrs['objectClass'][]= $class;
3195 }
3196 }
3199 /*! \brief Removes a given objectClass from the attrs entry
3200 *
3201 * Similar to add_objectClass, except that it removes the given
3202 * objectClasses. See it for the params.
3203 * */
3204 function remove_objectClass($classes, &$attrs)
3205 {
3206 if (isset($attrs['objectClass'])){
3207 /* Array? */
3208 if (is_array($classes)){
3209 $list= $classes;
3210 } else {
3211 $list= array($classes);
3212 }
3214 $tmp= array();
3215 foreach ($attrs['objectClass'] as $oc) {
3216 foreach ($list as $class){
3217 if (strtolower($oc) != strtolower($class)){
3218 $tmp[]= $oc;
3219 }
3220 }
3221 }
3222 $attrs['objectClass']= $tmp;
3223 }
3224 }
3227 /*! \brief Initialize a file download with given content, name and data type.
3228 * \param string data The content to send.
3229 * \param string name The name of the file.
3230 * \param string type The content identifier, default value is "application/octet-stream";
3231 */
3232 function send_binary_content($data,$name,$type = "application/octet-stream")
3233 {
3234 header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
3235 header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
3236 header("Cache-Control: no-cache");
3237 header("Pragma: no-cache");
3238 header("Cache-Control: post-check=0, pre-check=0");
3239 header("Content-type: ".$type."");
3241 $HTTP_USER_AGENT = $_SERVER['HTTP_USER_AGENT'];
3243 /* Strip name if it is a complete path */
3244 if (preg_match ("/\//", $name)) {
3245 $name= basename($name);
3246 }
3248 /* force download dialog */
3249 if (preg_match('/MSIE 5.5/', $HTTP_USER_AGENT) || preg_match('/MSIE 6.0/', $HTTP_USER_AGENT)) {
3250 header('Content-Disposition: filename="'.$name.'"');
3251 } else {
3252 header('Content-Disposition: attachment; filename="'.$name.'"');
3253 }
3255 echo $data;
3256 exit();
3257 }
3260 function reverse_html_entities($str,$type = ENT_QUOTES , $charset = "UTF-8")
3261 {
3262 if(is_string($str)){
3263 return(htmlentities($str,$type,$charset));
3264 }elseif(is_array($str)){
3265 foreach($str as $name => $value){
3266 $str[$name] = reverse_html_entities($value,$type,$charset);
3267 }
3268 }
3269 return($str);
3270 }
3273 /*! \brief Encode special string characters so we can use the string in \
3274 HTML output, without breaking quotes.
3275 \param string The String we want to encode.
3276 \return string The encoded String
3277 */
3278 function xmlentities($str)
3279 {
3280 if(is_string($str)){
3282 static $asc2uni= array();
3283 if (!count($asc2uni)){
3284 for($i=128;$i<256;$i++){
3285 # $asc2uni[chr($i)] = "&#x".dechex($i).";";
3286 }
3287 }
3289 $str = str_replace("&", "&", $str);
3290 $str = str_replace("<", "<", $str);
3291 $str = str_replace(">", ">", $str);
3292 $str = str_replace("'", "'", $str);
3293 $str = str_replace("\"", """, $str);
3294 $str = str_replace("\r", "", $str);
3295 $str = strtr($str,$asc2uni);
3296 return $str;
3297 }elseif(is_array($str)){
3298 foreach($str as $name => $value){
3299 $str[$name] = xmlentities($value);
3300 }
3301 }
3302 return($str);
3303 }
3306 /*! \brief Updates all accessTo attributes from a given value to a new one.
3307 For example if a host is renamed.
3308 \param String $from The source accessTo name.
3309 \param String $to The destination accessTo name.
3310 */
3311 function update_accessTo($from,$to)
3312 {
3313 global $config;
3314 $ldap = $config->get_ldap_link();
3315 $ldap->cd($config->current['BASE']);
3316 $ldap->search("(&(objectClass=trustAccount)(accessTo=".$from."))",array("objectClass","accessTo"));
3317 while($attrs = $ldap->fetch()){
3318 $new_attrs = array("accessTo" => array());
3319 $dn = $attrs['dn'];
3320 for($i = 0 ; $i < $attrs['objectClass']['count']; $i++){
3321 $new_attrs['objectClass'][] = $attrs['objectClass'][$i];
3322 }
3323 for($i = 0 ; $i < $attrs['accessTo']['count']; $i++){
3324 if($attrs['accessTo'][$i] == $from){
3325 if(!empty($to)){
3326 $new_attrs['accessTo'][] = $to;
3327 }
3328 }else{
3329 $new_attrs['accessTo'][] = $attrs['accessTo'][$i];
3330 }
3331 }
3332 $ldap->cd($dn);
3333 $ldap->modify($new_attrs);
3334 if (!$ldap->success()){
3335 msg_dialog::display(_("LDAP error"), msgPool::ldaperror($ldap->get_error(), $dn, LDAP_MOD, "update_accessTo($from,$to)"));
3336 }
3337 new log("modify","update_accessTo($from,$to)",$dn,array_keys($new_attrs),$ldap->get_error());
3338 }
3339 }
3342 /*! \brief Returns a random char */
3343 function get_random_char () {
3344 $randno = rand (0, 63);
3345 if ($randno < 12) {
3346 return (chr ($randno + 46)); // Digits, '/' and '.'
3347 } else if ($randno < 38) {
3348 return (chr ($randno + 53)); // Uppercase
3349 } else {
3350 return (chr ($randno + 59)); // Lowercase
3351 }
3352 }
3355 function cred_encrypt($input, $password) {
3357 $size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
3358 $iv = mcrypt_create_iv($size, MCRYPT_DEV_RANDOM);
3360 return bin2hex(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $password, $input, MCRYPT_MODE_ECB, $iv));
3362 }
3365 function cred_decrypt($input,$password) {
3366 $size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
3367 $iv = mcrypt_create_iv($size, MCRYPT_DEV_RANDOM);
3369 return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $password, pack("H*", $input), MCRYPT_MODE_ECB, $iv);
3370 }
3373 function get_object_info()
3374 {
3375 return(session::get('objectinfo'));
3376 }
3379 function set_object_info($str = "")
3380 {
3381 session::set('objectinfo',$str);
3382 }
3385 function isIpInNet($ip, $net, $mask) {
3386 // Move to long ints
3387 $ip= ip2long($ip);
3388 $net= ip2long($net);
3389 $mask= ip2long($mask);
3391 // Mask given IP with mask. If it returns "net", we're in...
3392 $res= $ip & $mask;
3394 return ($res == $net);
3395 }
3398 function get_next_id($attrib, $dn)
3399 {
3400 global $config;
3402 switch ($config->get_cfg_value("idAllocationMethod", "traditional")){
3403 case "pool":
3404 return get_next_id_pool($attrib);
3405 case "traditional":
3406 return get_next_id_traditional($attrib, $dn);
3407 }
3409 msg_dialog::display(_("Error"), _("Cannot allocate a free ID:")." "._("unknown idAllocation method!"), ERROR_DIALOG);
3410 return null;
3411 }
3414 function get_next_id_pool($attrib) {
3415 global $config;
3417 /* Fill informational values */
3418 $min= $config->get_cfg_value("${attrib}PoolMin", 10000);
3419 $max= $config->get_cfg_value("${attrib}PoolMax", 40000);
3421 /* Sanity check */
3422 if ($min >= $max) {
3423 msg_dialog::display(_("Error"), _("Cannot allocate a free ID:")." ".sprintf(_("%sPoolMin >= %sPoolMax!"), $attrib), ERROR_DIALOG);
3424 return null;
3425 }
3427 /* ID to skip */
3428 $ldap= $config->get_ldap_link();
3429 $id= null;
3431 /* Try to allocate the ID several times before failing */
3432 $tries= 3;
3433 while ($tries--) {
3435 /* Look for ID map entry */
3436 $ldap->cd ($config->current['BASE']);
3437 $ldap->search ("(&(objectClass=sambaUnixIdPool)($attrib=*))", array("$attrib"));
3439 /* If it does not exist, create one with these defaults */
3440 if ($ldap->count() == 0) {
3441 /* Fill informational values */
3442 $minUserId= $config->get_cfg_value("uidPoolMin", 10000);
3443 $minGroupId= $config->get_cfg_value("gidPoolMin", 10000);
3445 /* Add as default */
3446 $attrs= array("objectClass" => array("organizationalUnit", "sambaUnixIdPool"));
3447 $attrs["ou"]= "idmap";
3448 $attrs["uidNumber"]= $minUserId;
3449 $attrs["gidNumber"]= $minGroupId;
3450 $ldap->cd("ou=idmap,".$config->current['BASE']);
3451 $ldap->add($attrs);
3452 if ($ldap->error != "Success") {
3453 msg_dialog::display(_("Error"), _("Cannot create sambaUnixIdPool entry!"), ERROR_DIALOG);
3454 return null;
3455 }
3456 $tries++;
3457 continue;
3458 }
3459 /* Bail out if it's not unique */
3460 if ($ldap->count() != 1) {
3461 msg_dialog::display(_("Error"), _("Cannot allocate a free ID:")." "._("sambaUnixIdPool is not unique!"), ERROR_DIALOG);
3462 return null;
3463 }
3465 /* Store old attrib and generate new */
3466 $attrs= $ldap->fetch();
3467 $dn= $ldap->getDN();
3468 $oldAttr= $attrs[$attrib][0];
3469 $newAttr= $oldAttr + 1;
3471 /* Sanity check */
3472 if ($newAttr >= $max) {
3473 msg_dialog::display(_("Error"), _("Cannot allocate a free ID:")." "._("no ID available!"), ERROR_DIALOG);
3474 return null;
3475 }
3476 if ($newAttr < $min) {
3477 msg_dialog::display(_("Error"), _("Cannot allocate a free ID:")." "._("no ID available!"), ERROR_DIALOG);
3478 return null;
3479 }
3481 #FIXME: PHP is not able to do a modification of "del: .../add: ...", so this
3482 # is completely unsafe in the moment.
3483 #/* Remove old attr, add new attr */
3484 #$attrs= array($attrib => $oldAttr);
3485 #$ldap->rm($attrs, $dn);
3486 #if ($ldap->error != "Success") {
3487 # continue;
3488 #}
3489 $ldap->cd($dn);
3490 $ldap->modify(array($attrib => $newAttr));
3491 if ($ldap->error != "Success") {
3492 msg_dialog::display(_("Error"), _("Cannot allocate a free ID:")." ".$ldap->get_error(), ERROR_DIALOG);
3493 return null;
3494 } else {
3495 return $oldAttr;
3496 }
3497 }
3499 /* Bail out if we had problems getting the next id */
3500 if (!$tries) {
3501 msg_dialog::display(_("Error"), _("Cannot allocate a free ID:")." "._("maximum tries exceeded!"), ERROR_DIALOG);
3502 }
3504 return $id;
3505 }
3508 function get_next_id_traditional($attrib, $dn)
3509 {
3510 global $config;
3512 $ids= array();
3513 $ldap= $config->get_ldap_link();
3515 $ldap->cd ($config->current['BASE']);
3516 if (preg_match('/gidNumber/i', $attrib)){
3517 $oc= "posixGroup";
3518 } else {
3519 $oc= "posixAccount";
3520 }
3521 $ldap->search ("(&(objectClass=$oc)($attrib=*))", array("$attrib"));
3523 /* Get list of ids */
3524 while ($attrs= $ldap->fetch()){
3525 $ids[]= (int)$attrs["$attrib"][0];
3526 }
3528 /* Add the nobody id */
3529 $ids[]= 65534;
3531 /* get the ranges */
3532 $tmp = array('0'=> 1000);
3533 if (preg_match('/posixAccount/', $oc) && $config->get_cfg_value("uidNumberBase") != ""){
3534 $tmp= explode('-',$config->get_cfg_value("uidNumberBase"));
3535 } elseif($config->get_cfg_value("gidNumberBase") != ""){
3536 $tmp= explode('-',$config->get_cfg_value("gidNumberBase"));
3537 }
3539 /* Set hwm to max if not set - for backward compatibility */
3540 $lwm= $tmp[0];
3541 if (isset($tmp[1])){
3542 $hwm= $tmp[1];
3543 } else {
3544 $hwm= pow(2,32);
3545 }
3546 /* Find out next free id near to UID_BASE */
3547 if ($config->get_cfg_value("baseIdHook") == ""){
3548 $base= $lwm;
3549 } else {
3550 /* Call base hook */
3551 $base= get_base_from_hook($dn, $attrib);
3552 }
3553 for ($id= $base; $id++; $id < $hwm){
3554 if (!in_array($id, $ids)){
3555 return ($id);
3556 }
3557 }
3559 /* Should not happen */
3560 if ($id == $hwm){
3561 msg_dialog::display(_("Error"), _("Cannot allocate a free ID!"), ERROR_DIALOG);
3562 exit;
3563 }
3564 }
3567 /* Mark the occurance of a string with a span */
3568 function mark($needle, $haystack, $ignorecase= true)
3569 {
3570 $result= "";
3572 while (preg_match('/^(.*)('.preg_quote($needle).')(.*)$/i', $haystack, $matches)) {
3573 $result.= $matches[1]."<span class='mark'>".$matches[2]."</span>";
3574 $haystack= $matches[3];
3575 }
3577 return $result.$haystack;
3578 }
3580 // vim:tabstop=2:expandtab:shiftwidth=2:filetype=php:syntax:ruler:
3581 ?>