<?php
/*************************
  Coppermine Photo Gallery
  ************************
  Copyright (c) 2003-2006 Coppermine Dev Team
  v1.2 originally written by Gregory DEMAR

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.
  ********************************************
  Originally written for:
  Coppermine version: 1.4.10
  $Source$
  $Author: Floris Mouwen
  $Adapted from Author: twanfox $
  $Adapted from: LDAP login hack by Tobias 'twobee' Mathes
                                    <twobee at c-base dot org>
  $Date: 2010-04-29
**********************************************/

/***************************
 Features
 ***************************
 - This bridge allows for subtree searching for user accounts.
 - Host and port are configurable, as well as TLS support.
 - As this is a basic modification of the 'coppermine' bridge
   module, it supports nearly all the same features.
 - Added CACHE_PASSWORD to prevent password caching in the database
 - Update e-mailaddress from LDAP users to the Database
 *********************************************/

/***************************
 External Modifications
 ***************************
 - To allow for user-defined groups, the group manager file (groupmgr.php)
   must be hacked. Wherever there are checks for UDB_INTEGRATION ==
   'coppermine', a check for UDB_INTEGRATION == 'ldap' must also be added,
   as these are essentially the same.

 - This bridge module requires the following modifications to the database
   backend.

   For MySQL:
   ALTER TABLE `{$CONFIG['TABLE_PREFIX']}_users`
      ADD `ldap_user_password` VARCHAR( 40 ) NOT NULL AFTER `user_password`;

   This will permit this plugin to function with LDAP authentication and
   switch back to the Coppermine internal authentication without requiring
   mass password resets.

   In theory, we can reuse the 'user_password' field, but I didn't like that
   as it would make switching back impossible and require mass password
   resets.

 - Configuration directives in the 'config.inc.php' include:

   ldapserver			DNS name of the LDAP server, defaults to 'localhost'
   ldapport				Port for the LDAP server, defaults to 389
   ldaptls				True/False. Enable use of TLS, defaults to false.
   ldapdn (required)	Base container to use for user searching.
 *********************************************/

/***************************
 To Do List
 ***************************
 - Allow for listing of locally stored information about LDAP-based users
 - Ensure no other external files need modifications to support this
   'coppermine'-like bridge.
 - Allow for directory stored user-defined groups. This is likely to require
   a schema or some LDAP addin to enhance it enough to be distinct.
 - Is it possible to store the configuration information in the bridge manager?
   And, do we want to, or is $CONFIG sufficient?
 - Clean up the use of hard coded field names. We have a 'fields' record set,
   let's use this so that consistency is maintained. In reality, this should be
   done to the original 'coppermine' bridge module as well.
 *********************************************/


if (!defined('IN_COPPERMINE')) die('Not in Coppermine...');

// Switch that allows overriding the bridge manager with hard-coded values
define('USE_BRIDGEMGR', 1);
define('LDAP_DEBUG', 0);
define('CACHE_PASSWORD', 0);

require_once 'bridge/udb_base.inc.php';

class ldap_udb extends core_udb {

        function ldap_udb()
        {
                global $BRIDGE,$CONFIG;

                if (!USE_BRIDGEMGR) {

                        $this->boardurl = 'http://localhost/coppermine';
                        include_once('../include/config.inc.php');

                } else {
                        $this->boardurl = $CONFIG['site_url'];
                        $this->use_post_based_groups = @$BRIDGE['use_post_based_groups'];
                }

                // A hash that's a little specific to the client's configuration
                $this->client_id = md5($_SERVER['HTTP_USER_AGENT'].$_SERVER['SERVER_PROTOCOL'].$CONFIG['site_url']);

                $this->multigroups = 1;

                $this->group_overrride = !$this->use_post_based_groups;

                // LDAP connection settings
                $this->ldap = array(
                		'host' => $CONFIG['ldapserver'] ? $CONFIG['ldapserver'] : 'localhost',
                		'port' => $CONFIG['ldapport'] ? $CONFIG['ldapport'] : 389,
                		'use_tls' => $CONFIG['ldaptls'] ? $CONFIG['ldaptls'] : false,
                		'base_dn' => $CONFIG['ldapdn']
                );

                // Database connection settings
                $this->db = array(
                        'name' => $CONFIG['dbname'],
                        'host' => $CONFIG['dbserver'] ? $CONFIG['dbserver'] : 'localhost',
                        'user' => $CONFIG['dbuser'],
                        'password' => $CONFIG['dbpass'],
                        'prefix' =>$CONFIG['TABLE_PREFIX'],
                );

                // Board table names
                $this->table = array(
                        'users' => 'users',
                        'groups' => 'usergroups',
                        'sessions' => 'sessions'
                );

                // Derived full table names
                $this->usertable = '`' . $this->db['name'] . '`.' . $this->db['prefix'] . $this->table['users'];
                $this->groupstable =  '`' . $this->db['name'] . '`.' . $this->db['prefix'] . $this->table['groups'];
                $this->sessionstable =  '`' . $this->db['name'] . '`.' . $this->db['prefix'] . $this->table['sessions'];

                // Table field names
                $this->field = array(
                        'username' => 'user_name', // name of 'username' field in users table
                        'user_id' => 'user_id', // name of 'id' field in users table
                        'password' => 'ldap_user_password', // name of 'password' field in users table
                        'email' => 'user_email', // name of 'email' field in users table
                        'regdate' => 'UNIX_TIMESTAMP(user_regdate)', // name of 'registered' field in users table
                        'lastvisit' => 'UNIX_TIMESTAMP(user_lastvisit)', // last time user logged in
                        'active' => 'user_active', // is user account active?
                        'location' => "''", // name of 'location' field in users table
                        'website' => "''", // name of 'website' field in users table
                        'usertbl_group_id' => 'user_group', // name of 'group id' field in users table
                        'grouptbl_group_id' => 'group_id', // name of 'group id' field in groups table
                        'grouptbl_group_name' => 'group_name' // name of 'group name' field in groups table
                );

                // Pages to redirect to
                $this->page = array(
                        'register' => 'register.php',
                        'editusers' => 'usermgr.php',
                        'edituserprofile' => 'profile.php'
                );

                // Group ids - admin and guest only.
                $this->admingroups = array(1);
                $this->guestgroup = 3;

                // Connect to db
                $this->connect($CONFIG['LINK_ID']);
        }


		// Login function
		function login( $username = null, $password = null, $remember = false ) {
				global $CONFIG;

				// Create the session_id from concat(cookievalue,client_id)
				$session_id = $this->session_id.$this->client_id;

				// Connect to the LDAP directory. Accept port information for nonstandard configs.
				$ldap_connect = ldap_connect($this->ldap['host'], $this->ldap['port'])
				or die("Could not connect to LDAP server.");
				echo (LDAP_DEBUG) ? "Passed LDAP Connect<br />\r\n" : "";

				// Set our version level to that sufficient for TLS.
				ldap_set_option($ldap_connect, LDAP_OPT_PROTOCOL_VERSION, 3)
				or die("Could not set LDAP Version level to 3.");
				echo (LDAP_DEBUG) ? "Passed LDAP Set Option<br />\r\n" : "";

				// Need TLS? Enable it or die trying.
				if ($this->ldap['use_tls'])
				{
					ldap_start_tls($ldap_connect)
					or die("TLS Requested, could not start TLS session.");
				}
				echo (LDAP_DEBUG) ? "Passed LDAP Start TLS<br />\r\n" : "";

				// Use 'base_dn' as root to search for our user. Allows for nested user containers.
				$ldap_search = ldap_search($ldap_connect, $this->ldap['base_dn'], "(uid={$username})")
				or die("Could not locate user entry within LDAP directory.");
				echo (LDAP_DEBUG) ? "Passed LDAP Search<br />\r\n" : "";
				echo (LDAP_DEBUG) ? "Base DN : '{$this->ldap['base_dn']}'<br />\r\n" : "";
				echo (LDAP_DEBUG) ? "User UID: 'uid={$username}'<br />\r\n" : "";

				// Found a user entry, get the attributes for it.
				$ldap_dn = ldap_get_entries($ldap_connect, $ldap_search)
				or die("Could not get user entry from LDAP search.");
				echo (LDAP_DEBUG) ? "Passed LDAP Get Entries<br />\r\n" : "";
				echo (LDAP_DEBUG) ? "LDAP Count: '{$ldap_dn['count']}'<br />\r\n" : "";
                                
                                // Get the user e-mailadres from LDAP
                                $user_email = $ldap_dn[0]["mail"][0];
                                echo (LDAP_DEBUG) ? "E-Mail: '{$user_email}'<br />\r\n" : "";

				// With the located distinguished name, bind to the server.
				$ldap_bind = ldap_bind($ldap_connect, $ldap_dn[0]['dn'], $password);
				echo (LDAP_DEBUG) ? "Passed LDAP Bind<br />\r\n" : "";

				if ($ldap_bind)
				{
					echo (LDAP_DEBUG) ? "Session Authenticated with dn '{$ldap_dn[0]['dn']}'<br />\r\n" : "";
					// User Authenticated successfully.
					// Begin processing and integration into Coppermine tables.

					// Shall we trust that the calling page properly sanitizes our $username?
					// The original Coppermine plugin does, so should we.
					$query  = "SELECT user_id, user_name, user_active FROM {$this->usertable} ";
					$query .= "WHERE `user_active` = 'YES' AND `user_name` = '".$username."'";
					$results = cpg_db_query($query);

					if (mysql_num_rows($results))
					{
						// User exists in Coppermine database
						// Update our last visited time and our LDAP password hash.
						// The latter is necessary for authentication past the login page.
						$sql =  "UPDATE {$this->usertable} SET user_lastvisit = NOW()";

                                                // Cache password if needed or empy it
                                                if (CACHE_PASSWORD) {
                                                    $sql .= ", {$this->field['password']} = md5('{$password}')";
                                                } else {
                                                    $sql .= ", {$this->field['password']} = ''";
                                                }

                                                // Update emailaddress when available
                                                if ($user_email != null) $sql .= ", {$this->field['email']} = '{$user_email}'";

						$sql .= " WHERE user_name = '{$username}' AND user_active = 'YES'";
						cpg_db_query($sql, $this->link_id);

						$USER_DATA = mysql_fetch_assoc($results);
						mysql_free_result($results);
					}
					else
					{
						// User doesn't exist within Coppermine database. Insert new record.				
						$sql_ins = "INSERT INTO {$this->usertable} (user_regdate, user_active, user_name, user_email";

                                                // Cache password if needed
                                                if (CACHE_PASSWORD) $sql_ins .= ", {$this->field['password']}";

                                                $sql_ins .= ") VALUES (NOW(), 'YES', '{$username}', '{$user_email}'";
                                                
                                                // Cache password if needed
                                                if (CACHE_PASSWORD) $sql_ins .= ", md5('{$password}')";

                                                $sql_ins .=")";

						echo (LDAP_DEBUG) ? "Executing user insert into {$this->usertable}: '{$sql_ins}'<br />\r\n" : "";

						cpg_db_query($sql_ins, $this->link_id);

						$query  = "SELECT user_id, user_name, user_active FROM {$this->usertable} ";
						$query .= "WHERE `user_active` = 'YES' AND `user_name` = '".$username."'";
						$results = cpg_db_query($query);
						$USER_DATA = mysql_fetch_assoc($results);
						mysql_free_result($results);
					}

					// If this is a 'remember me' login set the remember field to true
					if ($remember) {
						$remember_sql = ",remember = '1' ";
					} else {
						$remember_sql = '';
					}

					// Update guest session with user's information
					$sql  = "UPDATE {$this->sessionstable} SET ";
					$sql .= "user_id={$USER_DATA['user_id']} ";
					$sql .= $remember_sql;
					$sql .= "WHERE session_id=md5('$session_id');";
					cpg_db_query($sql, $this->link_id);

					return $USER_DATA;
				}
				else
				{
					echo (LDAP_DEBUG) ? "Session Failed with dn '{$ldap_dn[0]['dn']}'<br />\r\n" : "";
					return false;
				}
		}


		// Logout function
		function logout() {

				// Revert authenticated session to a guest session
				$session_id = $this->session_id.$this->client_id;
				$sql  = "update {$this->sessionstable} set user_id = 0, remember=0 where session_id=md5('$session_id');";
				cpg_db_query($sql, $this->link_id);
		}

		function get_groups( &$user )
		{
			$groups = array($user['group_id'] - 100);

			$sql = "SELECT user_group_list FROM {$this->usertable} AS u WHERE {$this->field['user_id']}='{$user['id']}' and user_group_list <> '';";

			$result = cpg_db_query($sql, $this->link_id);

			if ($row = mysql_fetch_array($result)){
				$groups = array_merge($groups, explode(',', $row['user_group_list']));
			}

			mysql_free_result($result);

			return $groups;
		}

		// definition of actions required to convert a password from user database form to cookie form
		function udb_hash_db($password)
		{
			return $password;
		}


		// definition of how to extract id, name, group from a session cookie
		function session_extraction()
		{
				global $CONFIG;

				// Default anonymous values
				$id = 0;
				$pass = '';

				// Get the session cookie value
				$sessioncookie = $_COOKIE[$this->client_id];

				// Create the session id by concat(session_cookie_value, client_id)
				$session_id = $sessioncookie.$this->client_id;
				echo (LDAP_DEBUG) ? "Acquiring session ID<br />\r\n" : "";

                // Lifetime of 'remember me' session is 2 weeks
                $rememberme_life_time = time()-(CPG_WEEK*2);

                // Lifetime of normal session is 1 hour
                $session_life_time = time()-CPG_HOUR;
				echo (LDAP_DEBUG) ? "Determining Session Lifetime<br />\r\n" : "";

                // Delete old sessions
                $sql = "delete from {$this->sessionstable} where time<$session_life_time and remember=0;";
                cpg_db_query($sql, $this->link_id);
				echo (LDAP_DEBUG) ? "Deleting old sessions<br />\r\n" : "";

                // Delete stale 'remember me' sessions
                $sql = "delete from {$this->sessionstable} where time<$rememberme_life_time;";
                cpg_db_query($sql, $this->link_id);
				echo (LDAP_DEBUG) ? "Deleting stale 'remember me' sessions<br />\r\n" : "";

                // Check for valid session if session_cookie_value exists
                if ($sessioncookie) {
					echo (LDAP_DEBUG) ? "We have session cookie '{$sessioncookie}'<br />\r\n" : "";

                    // Check for valid session
                    $sql =  'select user_id from '.$this->sessionstable.' where session_id=md5("'.$session_id.'");';
                    $result = cpg_db_query($sql);
					echo (LDAP_DEBUG) ? "Finding sessions using: {$sql}<br />\r\n" : "";

                    // If session exists...
                    if (mysql_num_rows($result)) {
						echo (LDAP_DEBUG) ? "We found a session<br />\r\n" : "";

                        $row = mysql_fetch_assoc($result);
                        mysql_free_result($result);

                        $row['user_id'] = (int) $row['user_id'];
						echo (LDAP_DEBUG) ? "Identified user ID as '{$row['user_id']}'<br />\r\n" : "";

                        // Check if there's a user for this session
                        $sql =  "SELECT user_id AS id, ldap_user_password AS password ";
                        $sql .= "FROM {$this->usertable} ";
                        $sql .= "WHERE user_id=".$row['user_id'];
                        $result = cpg_db_query($sql, $this->link_id);
                        echo (LDAP_DEBUG) ? "Locating user entry using: {$sql}<br />\r\n" : "";

                        // If user exists, use the current session
                        if ($result) {
							echo (LDAP_DEBUG) ? "User entry exists. Using current session.<br />\r\n" : "";
                            $row = mysql_fetch_assoc($result);
                            mysql_free_result($result);

                            $pass = $row['password'];
                            $id = (int) $row['id'];
                            $this->session_id = $sessioncookie;

                        // If the user doesn't exist, use default guest credentials
                        }

                    // If not a valid session exists, create a new session
                    } else {

                        $this->create_session();
                    }

                // No session exists; create one
                } else {

                    $this->create_session();
                }

				echo (LDAP_DEBUG && ($id)) ? "Returning id '{$id}' and pass '{$pass}'<br />\r\n" : "";
                return ($id) ? array($id, $pass) : false;
        }


        // Function used to keep the session alive
        function session_update()
        {
                $session_id = $this->session_id.$this->client_id;
                $sql = "update {$this->sessionstable} set time='".time()."' where session_id=md5('$session_id');";
                cpg_db_query($sql);
        }


        // Create a new session with the cookie lifetime set to 2 weeks
        function create_session() {
                global $CONFIG;
                // start session
                $this->session_id = $this->generateId();
                $session_id = $this->session_id.$this->client_id;

                $sql =  'insert into '.$this->sessionstable.' (session_id, user_id, time, remember) values ';
                $sql .= '("'.md5($session_id).'", 0, "'.time().'", 0);';

                // insert the guest session
                cpg_db_query($sql, $this->link_id);

                // set the session cookie
                setcookie( $this->client_id, $this->session_id, time() + (CPG_WEEK*2), $CONFIG['cookie_path'] );
        }


        // Modified function taken from Mambo session class
        function generateId() {
                $failsafe = 20;
                $randnum = 0;
                while ($failsafe--) {
                        $randnum = md5( uniqid( microtime(), 1 ));
                        $session_id = $randnum.$this->client_id;
                        if ($randnum != "") {
                                $sql = "SELECT session_id FROM {$this->sessionstable} WHERE session_id=MD5('$session_id')";
								$result = cpg_db_query($sql, $this->link_id);
                                if (!mysql_num_rows($result)) {
                                        break;
                                }
                                mysql_free_result($result);
                        }
                }
                return $randnum;
        }


        // Gets user/guest count
        function get_session_users() {
                static $count = array();

                if (!$count) {
                        // Get guest count
                        $sql = "select count(user_id) as num_guests from {$this->sessionstable} where user_id=0;";
                        $result = cpg_db_query($sql, $this->link_id);
                        $count = mysql_fetch_assoc($result);

                        // Get authenticated user count
                        $sql = "select count(user_id) as num_users from {$this->sessionstable} where user_id>0;";
                        $result = cpg_db_query($sql, $this->link_id);
                        $count = array_merge(mysql_fetch_assoc($result), $count);
                }

                return $count;
        }


        /*
         * Overidden functions !!DO NOT REMOVE OR CPG WILL NOT WORK CORRECTLY!!
         */
        // definition of how to extract an id and password hash from a cookie
        function cookie_extraction()
        {
                return false;
        }

        // Register
        function register_page()
        {        }

        // Edit users
        function edit_users()
        {        }

        // View users
        function view_users()
        {        }

        // View user profile
        function view_profile($uid)
        {        }

        // Edit user profile
        function edit_profile($uid)
        {        }

        function login_page()
        {        }

        function logout_page() {
            $this->logout();
        }
		/* Note : we don't want to overide this - the groups need to be resynced to coppermine default after un-integration
         * Rebuttal: without overriding and removing Coppermine group deletion (see below) its impossible to add new groups to a non-bridged install.
         */

    	function synchronize_groups()
    	{
    		global $CONFIG ;

    		if ($this->use_post_based_groups){
    			// FIXME: Looks like this is where we go to do directory stored groups.
    			if ($this->group_overrride){
    				$udb_groups = $this->collect_groups();
    			} else {
    				$sql = "SELECT * FROM {$this->groupstable} WHERE {$this->field['grouptbl_group_name']} <> ''";

    				$result = cpg_db_query($sql, $this->link_id);

    				$udb_groups = array();

    				while ($row = mysql_fetch_assoc($result))
    				{
    					$udb_groups[$row[$this->field['grouptbl_group_id']]+100] = utf_ucfirst(utf_strtolower($row[$this->field['grouptbl_group_name']]));
    				}
    			}
    		} else {
    			$udb_groups = array(1 =>'Administrators', 2=> 'Registered', 3=>'Guests', 4=> 'Banned');
    		}

    		$result = cpg_db_query("SELECT group_id, group_name FROM {$CONFIG['TABLE_USERGROUPS']} WHERE 1");

    		while ($row = mysql_fetch_array($result)) {
    			$cpg_groups[$row['group_id']] = $row['group_name'];
    		}

    		mysql_free_result($result);
            /* Must be removed to allow new groups to be created in an unbridged install.
    		// Scan Coppermine groups that need to be deleted
    		foreach($cpg_groups as $c_group_id => $c_group_name) {
    			if ((!isset($udb_groups[$c_group_id]))) {
    				cpg_db_query("DELETE FROM {$CONFIG['TABLE_USERGROUPS']} WHERE group_id = '" . $c_group_id . "' LIMIT 1");
    				unset($cpg_groups[$c_group_id]);
    			}
    		}
    		*/

    		// Scan udb groups that need to be created inside Coppermine table
    		foreach($udb_groups as $i_group_id => $i_group_name) {
    			if ((!isset($cpg_groups[$i_group_id]))) {
    				// add admin info
    				$admin_access = in_array($i_group_id-100, $this->admingroups) ? '1' : '0';
    				cpg_db_query("INSERT INTO {$CONFIG['TABLE_USERGROUPS']} (group_id, group_name, has_admin_access) VALUES ('$i_group_id', '" . addslashes($i_group_name) . "', '$admin_access')");
    				$cpg_groups[$i_group_id] = $i_group_name;
    			}
    		}

    		// Update Group names
    		foreach($udb_groups as $i_group_id => $i_group_name) {
    			if ($cpg_groups[$i_group_id] != $i_group_name) {
    				cpg_db_query("UPDATE {$CONFIG['TABLE_USERGROUPS']} SET group_name = '" . addslashes($i_group_name) . "' WHERE group_id = '$i_group_id' LIMIT 1");
    			}
    		}
    		// fix admin grp
    		if (!$this->use_post_based_groups) cpg_db_query("UPDATE {$CONFIG['TABLE_USERGROUPS']} SET has_admin_access = '1' WHERE group_id = '1' LIMIT 1");

    	}

}

// and go !
$cpg_udb = new ldap_udb;
?>
