diff --git a/app/Console/Commands/LdapSync.php b/app/Console/Commands/LdapSync.php index 5c6c512355..46f1cd5bcf 100755 --- a/app/Console/Commands/LdapSync.php +++ b/app/Console/Commands/LdapSync.php @@ -1,14 +1,24 @@ + * + * @since 5.0.0 + */ class LdapSync extends Command { /** @@ -16,23 +26,79 @@ class LdapSync extends Command * * @var string */ - protected $signature = 'snipeit:ldap-sync {--location=} {--location_id=} {--base_dn=} {--summary} {--json_summary}'; + protected $signature = 'snipeit:ldap-sync + {--location= : A location name } + {--location_id= : A location id} + {--base_dn= : A diffrent base DN to use } + {--summary : Print summary } + {--json_summary : Print summary in json format } + {--dryrun : Run the sync process but don\'t update the database}'; /** * The console command description. * * @var string */ - protected $description = 'Command line LDAP sync'; + protected $description = 'Command line LDAP/AD sync'; + + /** + * An LdapAd instance. + * + * @var \App\Models\LdapAd + */ + private $ldap; + + /** + * LDAP settings collection. + * + * @var \Illuminate\Support\Collection + */ + private $settings = null; + + /** + * A default location collection. + * + * @var \Illuminate\Support\Collection + */ + private $defaultLocation = null; + + /** + * Mapped locations collection. + * + * @var \Illuminate\Support\Collection + */ + private $mappedLocations = null; + + /** + * The summary collection. + * + * @var \Illuminate\Support\Collection + */ + private $summary; + + /** + * Is dry-run? + * + * @var bool + */ + private $dryrun = false; + + /** + * Show users to be imported. + * + * @var array + */ + private $userlist = []; /** * Create a new command instance. - * - * @return void */ - public function __construct() + public function __construct(LdapAd $ldap) { parent::__construct(); + $this->ldap = $ldap; + $this->settings = $this->ldap->ldapSettings; + $this->summary = collect(); } /** @@ -42,206 +108,273 @@ class LdapSync extends Command */ public function handle() { - ini_set('max_execution_time', 600); //600 seconds = 10 minutes + ini_set('max_execution_time', '600'); //600 seconds = 10 minutes ini_set('memory_limit', '500M'); - $ldap_result_username = Setting::getSettings()->ldap_username_field; - $ldap_result_last_name = Setting::getSettings()->ldap_lname_field; - $ldap_result_first_name = Setting::getSettings()->ldap_fname_field; - - $ldap_result_active_flag = Setting::getSettings()->ldap_active_flag_field; - $ldap_result_emp_num = Setting::getSettings()->ldap_emp_num; - $ldap_result_email = Setting::getSettings()->ldap_email; - - try { - $ldapconn = Ldap::connectToLdap(); - Ldap::bindAdminToLdap($ldapconn); - } catch (\Exception $e) { - if ($this->option('json_summary')) { - $json_summary = [ "error" => true, "error_message" => $e->getMessage(), "summary" => [] ]; - $this->info(json_encode($json_summary)); - } - LOG::error($e); - return []; + if ($this->option('dryrun')) { + $this->dryrun = true; } - $summary = array(); + $this->checkIfLdapIsEnabled(); + $this->checkLdapConnetion(); + $this->setBaseDn(); + $this->getUserDefaultLocation(); + /* + * Use the default location if set, this is needed for the LDAP users sync page + */ + if (!$this->option('base_dn') && null == $this->defaultLocation) { + $this->getMappedLocations(); + } + $this->processLdapUsers(); - try { - if ($this->option('base_dn') != '') { - $search_base = $this->option('base_dn'); - LOG::debug('Importing users from specified base DN: \"'.$search_base.'\".'); - } else { - $search_base = null; - } - $results = Ldap::findLdapUsers($search_base); - } catch (\Exception $e) { - if ($this->option('json_summary')) { - $json_summary = [ "error" => true, "error_message" => $e->getMessage(), "summary" => [] ]; - $this->info(json_encode($json_summary)); - } - LOG::error($e); - return []; + // Print table of users + if ($this->dryrun) { + $this->info('The following users will be synced!'); + $headers = ['First Name', 'Last Name', 'Username', 'Email', 'Employee #', 'Location Id', 'Status']; + $this->table($headers, $this->summary->toArray()); } - /* Determine which location to assign users to by default. */ - $location = NULL; + return $this->getSummary(); + } - if ($this->option('location')!='') { - $location = Location::where('name', '=', $this->option('location'))->first(); - LOG::debug('Location name '.$this->option('location').' passed'); - LOG::debug('Importing to '.$location->name.' ('.$location->id.')'); - } elseif ($this->option('location_id')!='') { - $location = Location::where('id', '=', $this->option('location_id'))->first(); - LOG::debug('Location ID '.$this->option('location_id').' passed'); - LOG::debug('Importing to '.$location->name.' ('.$location->id.')'); - } + /** + * Generate the LDAP sync summary. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @return string + */ + private function getSummary(): string + { + if ($this->option('summary') && null === $this->dryrun) { + $this->summary->each(function ($item) { + $this->info('USER: '.$item['note']); - if (!isset($location)) { - LOG::debug('That location is invalid or a location was not provided, so no location will be assigned by default.'); - } - - /* Process locations with explicitly defined OUs, if doing a full import. */ - if ($this->option('base_dn')=='') { - // Retrieve locations with a mapped OU, and sort them from the shallowest to deepest OU (see #3993) - $ldap_ou_locations = Location::where('ldap_ou', '!=', '')->get()->toArray(); - $ldap_ou_lengths = array(); - - foreach ($ldap_ou_locations as $location) { - $ldap_ou_lengths[] = strlen($location["ldap_ou"]); - } - - array_multisort($ldap_ou_lengths, SORT_ASC, $ldap_ou_locations); - - if (sizeof($ldap_ou_locations) > 0) { - LOG::debug('Some locations have special OUs set. Locations will be automatically set for users in those OUs.'); - } - - // Inject location information fields - for ($i = 0; $i < $results["count"]; $i++) { - $results[$i]["ldap_location_override"] = false; - $results[$i]["location_id"] = 0; - } - - // Grab subsets based on location-specific DNs, and overwrite location for these users. - foreach ($ldap_ou_locations as $ldap_loc) { - $location_users = Ldap::findLdapUsers($ldap_loc["ldap_ou"]); - $usernames = array(); - for ($i = 0; $i < $location_users["count"]; $i++) { - if (array_key_exists($ldap_result_username, $location_users[$i])) { - $location_users[$i]["ldap_location_override"] = true; - $location_users[$i]["location_id"] = $ldap_loc["id"]; - $usernames[] = $location_users[$i][$ldap_result_username][0]; - } + if ('ERROR' === $item['status']) { + $this->error('ERROR: '.$item['note']); } - - // Delete located users from the general group. - foreach ($results as $key => $generic_entry) { - if ((is_array($generic_entry)) && (array_key_exists($ldap_result_username, $generic_entry))) { - if (in_array($generic_entry[$ldap_result_username][0], $usernames)) { - unset($results[$key]); - } - } - } - - $global_count = $results['count']; - $results = array_merge($location_users, $results); - $results['count'] = $global_count; - } - } - - /* Create user account entries in Snipe-IT */ - $tmp_pass = substr(str_shuffle("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), 0, 20); - $pass = bcrypt($tmp_pass); - - for ($i = 0; $i < $results["count"]; $i++) { - if (empty($ldap_result_active_flag) || $results[$i][$ldap_result_active_flag][0] == "TRUE") { - - $item = array(); - $item["username"] = isset($results[$i][$ldap_result_username][0]) ? $results[$i][$ldap_result_username][0] : ""; - $item["employee_number"] = isset($results[$i][$ldap_result_emp_num][0]) ? $results[$i][$ldap_result_emp_num][0] : ""; - $item["lastname"] = isset($results[$i][$ldap_result_last_name][0]) ? $results[$i][$ldap_result_last_name][0] : ""; - $item["firstname"] = isset($results[$i][$ldap_result_first_name][0]) ? $results[$i][$ldap_result_first_name][0] : ""; - $item["email"] = isset($results[$i][$ldap_result_email][0]) ? $results[$i][$ldap_result_email][0] : "" ; - $item["ldap_location_override"] = isset($results[$i]["ldap_location_override"]) ? $results[$i]["ldap_location_override"]:""; - $item["location_id"] = isset($results[$i]["location_id"]) ? $results[$i]["location_id"]:""; - - if ( array_key_exists('useraccountcontrol', $results[$i]) ) { - $enabled_accounts = [ - '512', '544', '66048', '66080', '262656', '262688', '328192', '328224' - ]; - $item['activated'] = ( in_array($results[$i]['useraccountcontrol'][0], $enabled_accounts) ) ? 1 : 0; - } else { - $item['activated'] = 0; - - // If there is no activated flag, assume this is handled via the OU and activate the users - if (empty($ldap_result_active_flag)) { - $item['activated'] = 1; - } - } - - // User exists - $item["createorupdate"] = 'updated'; - if (!$user = User::where('username', $item["username"])->first()) { - $user = new User; - $user->password = $pass; - $item["createorupdate"] = 'created'; - } - - // Create the user if they don't exist. - $user->first_name = e($item["firstname"]); - $user->last_name = e($item["lastname"]); - $user->username = e($item["username"]); - $user->email = e($item["email"]); - $user->employee_num = e($item["employee_number"]); - $user->activated = $item['activated']; - - if ($item['ldap_location_override'] == true) { - $user->location_id = $item['location_id']; - } elseif ((isset($location)) && (!empty($location))) { - - if ((is_array($location)) && (array_key_exists('id', $location))) { - $user->location_id = $location['id']; - } elseif (is_object($location)) { - $user->location_id = $location->id; - } - - } - - $user->notes = 'Imported from LDAP'; - $user->ldap_import = 1; - - $errors = ''; - - if ($user->save()) { - $item["note"] = $item["createorupdate"]; - $item["status"]='success'; - } else { - foreach ($user->getErrors()->getMessages() as $key => $err) { - $errors .= $err[0]; - } - $item["note"] = $errors; - $item["status"]='error'; - } - - array_push($summary, $item); - } - - } - - if ($this->option('summary')) { - for ($x = 0; $x < count($summary); $x++) { - if ($summary[$x]['status']=='error') { - $this->error('ERROR: '.$summary[$x]['firstname'].' '.$summary[$x]['lastname'].' (username: '.$summary[$x]['username'].') was not imported: '.$summary[$x]['note']); - } else { - $this->info('User '.$summary[$x]['firstname'].' '.$summary[$x]['lastname'].' (username: '.$summary[$x]['username'].') was '.strtoupper($summary[$x]['createorupdate']).'.'); - } - } - } else if ($this->option('json_summary')) { - $json_summary = [ "error" => false, "error_message" => "", "summary" => $summary ]; + }); + } elseif ($this->option('json_summary')) { + $json_summary = [ + 'error' => false, + 'error_message' => '', + 'summary' => $this->summary->toArray(), + ]; $this->info(json_encode($json_summary)); - } else { - return $summary; + } + + return ''; + } + + /** + * Create a new user or update an existing user. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @param \Adldap\Models\User $snipeUser + */ + private function updateCreateUser(AdldapUser $snipeUser): void + { + $user = $this->ldap->processUser($snipeUser, $this->defaultLocation, $this->mappedLocations); + $summary = [ + 'firstname' => $user->first_name, + 'lastname' => $user->last_name, + 'username' => $user->username, + 'employee_number' => $user->employee_num, + 'email' => $user->email, + 'location_id' => $user->location_id, + ]; + // Only update the database if is not a dry run + if (!$this->dryrun) { + if ($user->save()) { + $summary['note'] = ($user->wasRecentlyCreated ? 'CREATED' : 'UPDATED'); + $summary['status'] = 'SUCCESS'; + } else { + $errors = ''; + foreach ($user->getErrors()->getMessages() as $error) { + $errors .= $error[0]; + } + $summary['note'] = $userMsg.' was not imported. REASON: '.$errors; + $summary['status'] = 'ERROR'; + } + } + + $summary['note'] = ($user->getOriginal('username') ? 'UPDATED' : 'CREATED'); + $this->summary->push($summary); + } + + /** + * Process the users to update / create. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @param int $page The page to get the result set + */ + private function processLdapUsers(int $page=0): void + { + try { + $ldapUsers = $this->ldap->getLdapUsers($page); + } catch (Exception $e) { + $this->outputError($e); + exit($e->getMessage()); + } + + if (0 == $ldapUsers->count()) { + $msg = 'ERROR: No users found!'; + Log::error($msg); + if ($this->dryrun) { + $this->error($msg); + } + exit($msg); + } + + // Process each individual users + foreach ($ldapUsers as $user) { + $this->updateCreateUser($user); + } + + if ($ldapUsers->getCurrentPage() < $ldapUsers->getPages()) { + $this->processLdapUsers($ldapUsers->getCurrentPage() + 1); } } + + /** + * Get the mapped locations if a base_dn is provided. + * + * @author Wes Hulette + * + * @since 5.0.0 + */ + private function getMappedLocations() + { + $ldapOuLocation = Location::where('ldap_ou', '!=', '')->select(['id', 'ldap_ou'])->get(); + $locations = $ldapOuLocation->sortBy(function ($ou, $key) { + return strlen($ou->ldap_ou); + }); + if ($locations->count() > 0) { + $msg = 'Some locations have special OUs set. Locations will be automatically set for users in those OUs.'; + LOG::debug($msg); + if ($this->dryrun) { + $this->info($msg); + } + + $this->mappedLocations = $locations->pluck('ldap_ou', 'id'); + } + } + + /** + * Set the base dn if supplied. + * + * @author Wes Hulette + * + * @since 5.0.0 + */ + private function setBaseDn(): void + { + if ($this->option('base_dn')) { + $this->ldap->baseDn = $this->option('base_dn'); + $msg = sprintf('Importing users from specified base DN: "%s"', $this->ldap->baseDn); + LOG::debug($msg); + if ($this->dryrun) { + $this->info($msg); + } + } + } + + /** + * Get a default location id for imported users. + * + * @author Wes Hulette + * + * @since 5.0.0 + */ + private function getUserDefaultLocation(): void + { + $location = $this->option('location_id') ?? $this->option('location'); + if ($location) { + $userLocation = Location::where('name', '=', $location) + ->orWhere('id', '=', intval($location)) + ->select(['name', 'id']) + ->first(); + if ($userLocation) { + $msg = 'Importing users with default location: '.$userLocation->name.' ('.$userLocation->id.')'; + LOG::debug($msg); + + if ($this->dryrun) { + $this->info($msg); + } + + $this->defaultLocation = collect([ + $userLocation->id => $userLocation->name, + ]); + } else { + $msg = 'The supplied location is invalid!'; + LOG::error($msg); + if ($this->dryrun) { + $this->error($msg); + } + exit(0); + } + } + } + + /** + * Check if LDAP intergration is enabled. + * + * @author Wes Hulette + * + * @since 5.0.0 + */ + private function checkIfLdapIsEnabled(): void + { + if (false === $this->settings['ldap_enabled']) { + $msg = 'LDAP intergration is not enabled. Exiting sync process.'; + $this->info($msg); + Log::info($msg); + exit(0); + } + } + + /** + * Check to make sure we can access the server. + * + * @author Wes Hulette + * + * @since 5.0.0 + */ + private function checkLdapConnetion(): void + { + try { + $this->ldap->testLdapAdUserConnection(); + $this->ldap->testLdapAdBindConnection(); + } catch (Exception $e) { + $this->outputError($e); + exit(0); + } + } + + /** + * Output the json summary to the screen if enabled. + * + * @param Exception $error + */ + private function outputError($error): void + { + if ($this->option('json_summary')) { + $json_summary = [ + 'error' => true, + 'error_message' => $error->getMessage(), + 'summary' => [], + ]; + $this->info(json_encode($json_summary)); + } + $this->error($error->getMessage()); + LOG::error($error); + } } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index ad2d6dcfd3..3a68afdb1b 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -38,12 +38,10 @@ class Kernel extends ConsoleKernel /** * Define the application's command schedule. * - * @param \Illuminate\Console\Scheduling\Schedule $schedule - * @return void + * @param \Illuminate\Console\Scheduling\Schedule $schedule */ protected function schedule(Schedule $schedule) { - $schedule->command('snipeit:inventory-alerts')->daily(); $schedule->command('snipeit:expiring-alerts')->daily(); $schedule->command('snipeit:expected-checkin')->daily(); @@ -53,9 +51,7 @@ class Kernel extends ConsoleKernel /** * This method is required by Laravel to handle any console routes - * that are defined in routes/console.php - * - * @return void + * that are defined in routes/console.php. */ protected function commands() { diff --git a/app/Http/Controllers/Api/SettingsController.php b/app/Http/Controllers/Api/SettingsController.php index 3d2723f8d8..bbc8199011 100644 --- a/app/Http/Controllers/Api/SettingsController.php +++ b/app/Http/Controllers/Api/SettingsController.php @@ -2,106 +2,88 @@ namespace App\Http\Controllers\Api; -use Illuminate\Http\Request; -use App\Http\Controllers\Controller; -use App\Models\Ldap; -use Validator; -use App\Models\Setting; -use Mail; -use App\Notifications\SlackTest; -use Notification; -use App\Notifications\MailTest; -use App\Http\Transformers\LoginAttemptsTransformer; use DB; +use Mail; +use Validator; +use Notification; +use App\Models\Ldap; +use App\Models\LdapAd; +use App\Models\Setting; +use Illuminate\Http\Request; +use App\Notifications\MailTest; +use App\Notifications\SlackTest; +use Illuminate\Http\JsonResponse; +use Illuminate\Support\Facades\Log; +use App\Http\Controllers\Controller; +use App\Http\Transformers\LoginAttemptsTransformer; class SettingsController extends Controller { - - public function ldaptest() - { - - if (Setting::getSettings()->ldap_enabled!='1') { - \Log::debug('LDAP is not enabled cannot test.'); + /** + * Test the ldap settings + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @param App\Models\LdapAd $ldap + * + * @return \Illuminate\Http\JsonResponse + */ + public function ldapAdSettingsTest(LdapAd $ldap): JsonResponse + { + if($ldap->ldapSettings['ldap_enabled'] === false) { + Log::info('LDAP is not enabled cannot test.'); return response()->json(['message' => 'LDAP is not enabled, cannot test.'], 400); } - \Log::debug('Preparing to test LDAP connection'); + // The connect, bind and resulting users message + $message = []; + Log::info('Preparing to test LDAP user login'); + // Test user can connect to the LDAP server try { - $connection = Ldap::connectToLdap(); - try { - \Log::debug('attempting to bind to LDAP for LDAP test'); - Ldap::bindAdminToLdap($connection); - return response()->json(['message' => 'It worked!'], 200); - } catch (\Exception $e) { - \Log::debug('Bind failed'); - return response()->json(['message' => $e->getMessage()], 400); - //return response()->json(['message' => $e->getMessage()], 500); - } - } catch (\Exception $e) { - \Log::debug('Connection failed'); - return response()->json(['message' => $e->getMessage()], 600); + $ldap->testLdapAdUserConnection(); + $message['login'] = [ + 'message' => 'Successfully connected to LDAP server.' + ]; + } catch (\Exception $ex) { + return response()->json([ + 'message' => 'Error logging into LDAP server, error: ' . $ex->getMessage() . ' - Verify your that your username and password are correct' + ], 400); } - - } - - public function ldaptestlogin(Request $request) - { - - if (Setting::getSettings()->ldap_enabled!='1') { - \Log::debug('LDAP is not enabled. Cannot test.'); - return response()->json(['message' => 'LDAP is not enabled, cannot test.'], 400); - } - - - $rules = array( - 'ldaptest_user' => 'required', - 'ldaptest_password' => 'required' - ); - - $validator = Validator::make($request->all(), $rules); - if ($validator->fails()) { - \Log::debug('LDAP Validation test failed.'); - $validation_errors = implode(' ',$validator->errors()->all()); - return response()->json(['message' => $validator->errors()->all()], 400); - } - - - \Log::debug('Preparing to test LDAP login'); + Log::info('Preparing to test LDAP bind connection'); + // Test user can bind to the LDAP server try { - $connection = Ldap::connectToLdap(); - try { - Ldap::bindAdminToLdap($connection); - \Log::debug('Attempting to bind to LDAP for LDAP test'); - try { - $ldap_user = Ldap::findAndBindUserLdap($request->input('ldaptest_user'), $request->input('ldaptest_password')); - if ($ldap_user) { - \Log::debug('It worked! '. $request->input('ldaptest_user').' successfully binded to LDAP.'); - return response()->json(['message' => 'It worked! '. $request->input('ldaptest_user').' successfully binded to LDAP.'], 200); - } - return response()->json(['message' => 'Login Failed. '. $request->input('ldaptest_user').' did not successfully bind to LDAP.'], 400); - - } catch (\Exception $e) { - \Log::debug('LDAP login failed'); - return response()->json(['message' => $e->getMessage()], 400); - } - - } catch (\Exception $e) { - \Log::debug('Bind failed'); - return response()->json(['message' => $e->getMessage()], 400); - //return response()->json(['message' => $e->getMessage()], 500); - } - } catch (\Exception $e) { - \Log::debug('Connection failed'); - return response()->json(['message' => $e->getMessage()], 500); + $ldap->testLdapAdBindConnection(); + $message['bind'] = [ + 'message' => 'Successfully binded to LDAP server.' + ]; + } catch (\Exception $ex) { + return response()->json([ + 'message' => 'Error binding to LDAP server, error: ' . $ex->getMessage() + ], 400); } + Log::info('Preparing to get sample user set from LDAP directory'); + // Get a sample of 10 users so user can verify the data is correct + try { + $users = $ldap->testUserImportSync(); + $message['user_sync'] = [ + 'users' => $users + ]; + } catch (\Exception $ex) { + $message['user_sync'] = [ + 'message' => 'Error getting users from LDAP directory, error: ' . $ex->getMessage() + ]; + return response()->json($message, 400); + } + return response()->json($message, 200); } - public function slacktest() { diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 4bcb8fe011..503cf8ac48 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -16,6 +16,7 @@ use Redirect; use Log; use View; use PragmaRX\Google2FA\Google2FA; +use App\Models\LdapAd; /** * This controller handles authentication for the user, including local @@ -39,15 +40,24 @@ class LoginController extends Controller */ protected $redirectTo = '/'; + /** + * An LdapAd instance + * + * @var \App\Models\LdapAd + */ + protected $ldapAd; + /** * Create a new authentication controller instance. * * @return void */ - public function __construct() + public function __construct(LdapAd $ldapAd) { $this->middleware('guest', ['except' => ['logout','postTwoFactorAuth','getTwoFactorAuth','getTwoFactorEnroll']]); \Session::put('backUrl', \URL::previous()); + + $this->ldapAd = $ldapAd; } function showLoginForm(Request $request) @@ -64,6 +74,29 @@ class LoginController extends Controller return view('auth.login'); } + /** + * Log in a user by LDAP + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @param Request $request + * + * @return User + * + * @throws Exception + */ + private function loginViaLdap(Request $request): User + { + try { + return $this->ldapAd->ldapLogin($request->input('username'), $request->input('password')); + } catch (\Exception $ex) { + LOG::debug("LDAP user login: " . $ex->getMessage()); + throw new \Exception($ex->getMessage()); + } + } + private function loginViaRemoteUser(Request $request) { $remote_user = $request->server('REMOTE_USER'); @@ -85,53 +118,6 @@ class LoginController extends Controller } } - private function loginViaLdap(Request $request) - { - LOG::debug("Binding user to LDAP."); - $ldap_user = Ldap::findAndBindUserLdap($request->input('username'), $request->input('password')); - if (!$ldap_user) { - LOG::debug("LDAP user ".$request->input('username')." not found in LDAP or could not bind"); - throw new \Exception("Could not find user in LDAP directory"); - } else { - LOG::debug("LDAP user ".$request->input('username')." successfully bound to LDAP"); - } - - // Check if the user already exists in the database and was imported via LDAP - $user = User::where('username', '=', Input::get('username'))->whereNull('deleted_at')->where('ldap_import', '=', 1)->where('activated', '=', '1')->first(); - LOG::debug("Local auth lookup complete"); - - // The user does not exist in the database. Try to get them from LDAP. - // If user does not exist and authenticates successfully with LDAP we - // will create it on the fly and sign in with default permissions - if (!$user) { - LOG::debug("Local user ".Input::get('username')." does not exist"); - LOG::debug("Creating local user ".Input::get('username')); - - if ($user = Ldap::createUserFromLdap($ldap_user)) { //this handles passwords on its own - LOG::debug("Local user created."); - } else { - LOG::debug("Could not create local user."); - throw new \Exception("Could not create local user"); - } - // If the user exists and they were imported from LDAP already - } else { - LOG::debug("Local user ".$request->input('username')." exists in database. Updating existing user against LDAP."); - - $ldap_attr = Ldap::parseAndMapLdapAttributes($ldap_user); - - if (Setting::getSettings()->ldap_pw_sync=='1') { - $user->password = bcrypt($request->input('password')); - } - - $user->email = $ldap_attr['email']; - $user->first_name = $ldap_attr['firstname']; - $user->last_name = $ldap_attr['lastname']; - $user->save(); - } // End if(!user) - return $user; - } - - /** * Account sign in form processing. * @@ -163,6 +149,7 @@ class LoginController extends Controller if (Setting::getSettings()->ldap_enabled=='1') { LOG::debug("LDAP is enabled."); try { + LOG::debug("Attempting to log user in by LDAP authentication."); $user = $this->loginViaLdap($request); Auth::login($user, true); diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index 78fb759bde..6676a11eb7 100755 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -943,7 +943,7 @@ class SettingsController extends Controller $setting->custom_forgot_pass_url = $request->input('custom_forgot_pass_url'); if ($setting->save()) { - return redirect()->route('settings.index') + return redirect()->route('settings.ldap.index') ->with('success', trans('admin/settings/message.update.success')); } diff --git a/app/Http/Controllers/Users/LDAPImportController.php b/app/Http/Controllers/Users/LDAPImportController.php index 4c91d0c8ae..a5774f478a 100644 --- a/app/Http/Controllers/Users/LDAPImportController.php +++ b/app/Http/Controllers/Users/LDAPImportController.php @@ -6,25 +6,44 @@ use App\Models\Ldap; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Artisan; +use App\Models\LdapAd; class LDAPImportController extends Controller { + /** + * An Ldap instance. + * + * @var LdapAd + */ + protected $ldap; /** - * Return view for LDAP import + * __construct. + * + * @param LdapAd $ldap + */ + public function __construct(LdapAd $ldap) + { + $this->ldap = $ldap; + } + + /** + * Return view for LDAP import. * * @author Aladin Alaily - * @since [v1.8] + * @author Wes Hulette + * + * @since 5.0.0 + * * @return \Illuminate\Contracts\View\View + * * @throws \Illuminate\Auth\Access\AuthorizationException */ public function create() { $this->authorize('update', User::class); try { - $ldapconn = Ldap::connectToLdap(); - Ldap::bindAdminToLdap($ldapconn); - + $this->ldap->testLdapAdUserConnection(); } catch (\Exception $e) { return redirect()->route('users.index')->with('error', $e->getMessage()); } @@ -32,19 +51,21 @@ class LDAPImportController extends Controller return view('users/ldap'); } - /** * LDAP form processing. * * @author Aladin Alaily - * @since [v1.8] + * @author Wes Hulette + * + * @since 5.0.0 + * * @return \Illuminate\Http\RedirectResponse */ public function store(Request $request) { // Call Artisan LDAP import command. $location_id = $request->input('location_id'); - Artisan::call('snipeit:ldap-sync', ['--location_id' => $location_id, '--json_summary' => true]); + Artisan::call('snipeit:ldapAd-sync', ['--location_id' => $location_id, '--json_summary' => true]); // Collect and parse JSON summary. $ldap_results_json = Artisan::output(); @@ -54,8 +75,9 @@ class LDAPImportController extends Controller if ($ldap_results['error']) { return redirect()->back()->withInput()->with('error', $ldap_results['error_message']); } + return redirect()->route('ldap/user') - ->with('success', "LDAP Import successful.") + ->with('success', 'LDAP Import successful.') ->with('summary', $ldap_results['summary']); } } diff --git a/app/Models/Ldap.php b/app/Models/Ldap.php deleted file mode 100644 index 8fb938aa1f..0000000000 --- a/app/Models/Ldap.php +++ /dev/null @@ -1,292 +0,0 @@ - Settings. - * - * @author [A. Gianotto] [] - * @since [v3.0] - * @return connection - */ - - public static function connectToLdap() - { - - $ldap_host = Setting::getSettings()->ldap_server; - $ldap_version = Setting::getSettings()->ldap_version; - $ldap_server_cert_ignore = Setting::getSettings()->ldap_server_cert_ignore; - $ldap_use_tls = Setting::getSettings()->ldap_tls; - - - // If we are ignoring the SSL cert we need to setup the environment variable - // before we create the connection - if ($ldap_server_cert_ignore=='1') { - putenv('LDAPTLS_REQCERT=never'); - } - - // If the user specifies where CA Certs are, make sure to use them - if (env("LDAPTLS_CACERT")) { - putenv("LDAPTLS_CACERT=".env("LDAPTLS_CACERT")); - } - - $connection = @ldap_connect($ldap_host); - - if (!$connection) { - throw new Exception('Could not connect to LDAP server at '.$ldap_host.'. Please check your LDAP server name and port number in your settings.'); - } - - // Needed for AD - ldap_set_option($connection, LDAP_OPT_REFERRALS, 0); - ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION, $ldap_version); - ldap_set_option($connection, LDAP_OPT_NETWORK_TIMEOUT, 20); - - if ($ldap_use_tls=='1') { - ldap_start_tls($connection); - } - - return $connection; - } - - - /** - * Binds/authenticates the user to LDAP, and returns their attributes. - * - * @author [A. Gianotto] [] - * @since [v3.0] - * @param $username - * @param $password - * @param bool|false $user - * @return bool true if the username and/or password provided are valid - * false if the username and/or password provided are invalid - * array of ldap_attributes if $user is true - * - */ - static function findAndBindUserLdap($username, $password) - { - $settings = Setting::getSettings(); - $connection = Ldap::connectToLdap(); - $ldap_username_field = $settings->ldap_username_field; - $baseDn = $settings->ldap_basedn; - $userDn = $ldap_username_field.'='.$username.','.$settings->ldap_basedn; - - if ($settings->is_ad =='1') { - // Check if they are using the userprincipalname for the username field. - // If they are, we can skip building the UPN to authenticate against AD - if ($ldap_username_field=='userprincipalname') { - $userDn = $username; - } else { - // In case they haven't added an AD domain - $userDn = ($settings->ad_domain != '') ? $username.'@'.$settings->ad_domain : $username.'@'.$settings->email_domain; - } - - } - - \Log::debug('Attempting to login using distinguished name:'.$userDn); - - - $filterQuery = $settings->ldap_auth_filter_query . $username; - - if (!$ldapbind = @ldap_bind($connection, $userDn, $password)) { - return false; - } - - if (!$results = ldap_search($connection, $baseDn, $filterQuery)) { - throw new Exception('Could not search LDAP: '); - } - - if (!$entry = ldap_first_entry($connection, $results)) { - return false; - } - - if (!$user = ldap_get_attributes($connection, $entry)) { - return false; - } - - return $user; - - } - - - /** - * Binds/authenticates an admin to LDAP for LDAP searching/syncing. - * Here we also return a better error if the app key is donked. - * - * @author [A. Gianotto] [] - * @since [v3.0] - * @param bool|false $user - * @return bool true if the username and/or password provided are valid - * false if the username and/or password provided are invalid - * - */ - static function bindAdminToLdap($connection) - { - - $ldap_username = Setting::getSettings()->ldap_uname; - - // Lets return some nicer messages for users who donked their app key, and disable LDAP - try { - $ldap_pass = \Crypt::decrypt(Setting::getSettings()->ldap_pword); - } catch (Exception $e) { - - throw new Exception('Your app key has changed! Could not decrypt LDAP password using your current app key, so LDAP authentication has been disabled. Login with a local account, update the LDAP password and re-enable it in Admin > Settings.'); - } - - - if (!$ldapbind = @ldap_bind($connection, $ldap_username, $ldap_pass)) { - throw new Exception('Could not bind to LDAP: '.ldap_error($connection)); - } - - } - - - /** - * Parse and map LDAP attributes based on settings - * - * @author [A. Gianotto] [] - * @since [v3.0] - * - * @param $ldapatttibutes - * @return array|bool - */ - static function parseAndMapLdapAttributes($ldapatttibutes) - { - //Get LDAP attribute config - $ldap_result_username = Setting::getSettings()->ldap_username_field; - $ldap_result_emp_num = Setting::getSettings()->ldap_emp_num; - $ldap_result_last_name = Setting::getSettings()->ldap_lname_field; - $ldap_result_first_name = Setting::getSettings()->ldap_fname_field; - $ldap_result_email = Setting::getSettings()->ldap_email; - - // Get LDAP user data - $item = array(); - $item["username"] = isset($ldapatttibutes[$ldap_result_username][0]) ? $ldapatttibutes[$ldap_result_username][0] : ""; - $item["employee_number"] = isset($ldapatttibutes[$ldap_result_emp_num][0]) ? $ldapatttibutes[$ldap_result_emp_num][0] : ""; - $item["lastname"] = isset($ldapatttibutes[$ldap_result_last_name][0]) ? $ldapatttibutes[$ldap_result_last_name][0] : ""; - $item["firstname"] = isset($ldapatttibutes[$ldap_result_first_name][0]) ? $ldapatttibutes[$ldap_result_first_name][0] : ""; - $item["email"] = isset($ldapatttibutes[$ldap_result_email][0]) ? $ldapatttibutes[$ldap_result_email][0] : "" ; - - return $item; - - - } - - /** - * Create user from LDAP attributes - * - * @author [A. Gianotto] [] - * @since [v3.0] - * @param $ldapatttibutes - * @return array|bool - */ - static function createUserFromLdap($ldapatttibutes) - { - $item = Ldap::parseAndMapLdapAttributes($ldapatttibutes); - - - // Create user from LDAP data - if (!empty($item["username"])) { - $user = new User; - $user->first_name = $item["firstname"]; - $user->last_name = $item["lastname"]; - $user->username = $item["username"]; - $user->email = $item["email"]; - - if (Setting::getSettings()->ldap_pw_sync=='1') { - $user->password = bcrypt(Input::get("password")); - } else { - $pass = substr(str_shuffle("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), 0, 25); - $user->password = bcrypt($pass); - } - - $user->activated = 1; - $user->ldap_import = 1; - $user->notes = 'Imported on first login from LDAP'; - - if ($user->save()) { - return $user; - } else { - LOG::debug('Could not create user.'.$user->getErrors()); - throw new Exception("Could not create user: ".$user->getErrors()); - } - } - - return false; - - } - - /** - * Searches LDAP - * - * @author [A. Gianotto] [] - * @since [v3.0] - * @param $ldapatttibutes - * @param $base_dn - * @return array|bool - */ - static function findLdapUsers($base_dn = null) - { - - $ldapconn = Ldap::connectToLdap(); - $ldap_bind = Ldap::bindAdminToLdap($ldapconn); - // Default to global base DN if nothing else is provided. - if (is_null($base_dn)) { - $base_dn = Setting::getSettings()->ldap_basedn; - } - $filter = Setting::getSettings()->ldap_filter; - - // Set up LDAP pagination for very large databases - $page_size = 500; - $cookie = ''; - $result_set = array(); - $global_count = 0; - - // Perform the search - do { - - // Paginate (non-critical, if not supported by server) - if (!$ldap_paging = @ldap_control_paged_result($ldapconn, $page_size, false, $cookie)) { - throw new Exception('Problem with your LDAP connection. Try checking the Use TLS setting in Admin > Settings. '); - } - - - $search_results = ldap_search($ldapconn, $base_dn, '('.$filter.')'); - - if (!$search_results) { - return redirect()->route('users.index')->with('error', trans('admin/users/message.error.ldap_could_not_search').ldap_error($ldapconn)); - } - - // Get results from page - $results = ldap_get_entries($ldapconn, $search_results); - if (!$results) { - return redirect()->route('users.index')->with('error', trans('admin/users/message.error.ldap_could_not_get_entries').ldap_error($ldapconn)); - } - - // Add results to result set - $global_count += $results['count']; - $result_set = array_merge($result_set, $results); - - @ldap_control_paged_result_response($ldapconn, $search_results, $cookie); - - } while ($cookie !== null && $cookie != ''); - - - // Clean up after search - $result_set['count'] = $global_count; - $results = $result_set; - ldap_control_paged_result($ldapconn, 0); - - return $results; - - - } -} diff --git a/app/Models/LdapAd.php b/app/Models/LdapAd.php new file mode 100644 index 0000000000..08730a91d6 --- /dev/null +++ b/app/Models/LdapAd.php @@ -0,0 +1,431 @@ + + * + * @since 5.0.0 + */ +class LdapAd extends LdapAdConfiguration +{ + use UserTrait; + + /** + * @see https://wdmsb.wordpress.com/2014/12/03/descriptions-of-active-directory-useraccountcontrol-value/ + */ + const AD_USER_ACCOUNT_CONTROL_FLAGS = ['512', '544', '66048', '66080', '262656', '262688', '328192', '328224']; + + /** + * The LDAP results per page. + */ + const PAGE_SIZE = 500; + + /** + * A base dn. + * + * @var string + */ + public $baseDn = null; + + /** + * Adldap instance. + * + * @var \Adldap\Adldap + */ + protected $ldap; + + /** + * __construct. + */ + public function __construct() + { + parent::__construct(); + + $this->ldap = new Adldap(); + $this->ldap->addProvider($this->ldapConfig); + } + + /** + * Create a user if they successfully login to the LDAP server. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @param string $username + * @param string $password + * + * @return \App\Models\User + * + * @throws Exception + */ + public function ldapLogin(string $username, string $password): User + { + try { + $this->ldap->auth()->attempt($username, $password); + } catch (Exception $e) { + Log::error($e->getMessage()); + throw new Exception('Unable to validate user credentials!'); + } + + // Should we sync the logged in user + if ($this->isLdapSync($record)) { + try { + Log::debug('Attempting to find user in LDAP directory'); + $record = $this->ldap->search()->findBy($this->ldapSettings['ldap_username_field'], $username); + } catch (ModelNotFoundException $e) { + Log::error($e->getMessage()); + throw new Exception('Unable to find user in LDAP directory!'); + } + + $this->syncUserLdapLogin($record, $password); + } + + return User::where('username', $username) + ->whereNull('deleted_at')->where('ldap_import', '=', 1) + ->where('activated', '=', '1')->first(); + } + + /** + * Set the user information based on the LDAP settings. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @param \Adldap\Models\User $user + * @param null|Collection $defaultLocation + * @param null|Collection $mappedLocations + * + * @return null|\App\Models\User + */ + public function processUser(AdldapUser $user, ?Collection $defaultLocation=null, ?Collection $mappedLocations=null): ?User + { + // Only sync active users + if ($this->isLdapSync($user)) { + $snipeUser = []; + $snipeUser['username'] = $user->{$this->ldapSettings['ldap_username_field']}[0] ?? ''; + $snipeUser['employee_number'] = $user->{$this->ldapSettings['ldap_emp_num']}[0] ?? ''; + $snipeUser['lastname'] = $user->{$this->ldapSettings['ldap_lname_field']}[0] ?? ''; + $snipeUser['firstname'] = $user->{$this->ldapSettings['ldap_fname_field']}[0] ?? ''; + $snipeUser['email'] = $user->{$this->ldapSettings['ldap_email']}[0] ?? ''; + $snipeUser['location_id'] = $this->getLocationId($user, $defaultLocation, $mappedLocations); + $snipeUser['activated'] = $this->getActiveStatus($user); + + return $this->setUserModel($snipeUser); + } + + // We are not syncing user info + return null; + } + + /** + * Set the User model information. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @param array $userInfo The user info to save to the database + * + * @return \App\Models\User + */ + public function setUserModel(array $userInfo): User + { + // If the username exists, return the user object, otherwise create a new user object + $user = User::firstOrNew([ + 'username' => $userInfo['username'], + ]); + $user->username = $user->username ?? trim($userInfo['username']); + $user->password = $user->password ?? $this->generateEncyrptedPassword(); + $user->first_name = trim($userInfo['firstname']); + $user->last_name = trim($userInfo['lastname']); + $user->email = trim($userInfo['email']); + $user->employee_num = trim($userInfo['employee_number']); + $user->activated = $userInfo['activated']; + $user->location_id = $userInfo['location_id']; + $user->notes = 'Imported from LDAP'; + $user->ldap_import = 1; + + return $user; + } + + /** + * Sync a user who has logged in by LDAP. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @param \Adldap\Models\User $record + * @param string $password + * + * @throws Exception + */ + private function syncUserLdapLogin(AdldapUser $record, string $password): void + { + $user = $this->processUser($record); + + if (is_null($user->last_login)) { + $user->notes = 'Imported on first login from LDAP2'; + } + + if ($this->ldapSettings['ldap_pw_sync']) { + Log::debug('Syncing users password with LDAP directory.'); + $user->password = bcrypt($password); + } + + if (!$user->save()) { + Log::debug('Could not save user. '.$user->getErrors()); + throw new Exception('Could not save user: '.$user->getErrors()); + } + } + + /** + * Check to see if we should sync the user with the LDAP directory. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @param \Adldap\Models\User $user + * + * @return bool + */ + private function isLdapSync(AdldapUser $user): bool + { + return (false === $this->ldapSettings['ldap_active_flag']) + || ('true' == strtolower($user->{$this->ldapSettings['ldap_active_flag']}[0])); + } + + /** + * Set the active status of the user. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @param \Adldap\Models\User $user + * + * @return int + */ + private function getActiveStatus(AdldapUser $user): int + { + $activeStatus = 0; + /* + * Check to see if we are connected to an AD server + * if so, check the Active Directory User Account Control Flags + */ + if ($this->ldapSettings['is_ad']) { + $activeStatus = (in_array($user->getUserAccountControl(), self::AD_USER_ACCOUNT_CONTROL_FLAGS)) ? 1 : 0; + } else { + // If there is no activated flag, assume this is handled via the OU and activate the users + if (false === $this->ldapSettings['ldap_active_flag']) { + $activeStatus = 1; + } + } + + return $activeStatus; + } + + /** + * Get a default selected location, or a OU mapped location if available. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @param Adldap\Models\User $user + * @param Collection|null $defaultLocation + * @param Collection|null $mappedLocations + * + * @return null|int + */ + private function getLocationId(AdldapUser $user, ?Collection $defaultLocation, ?Collection $mappedLocations): ?int + { + $locationId = null; + // Set the users default locations, if set + if ($defaultLocation) { + $locationId = $defaultLocation->keys()->first(); + } + + // Check to see if the user is in a mapped location + if ($mappedLocations) { + $location = $mappedLocations->filter(function ($value, $key) use ($user) { + if ($user->inGroup([$value], true)) { + return $key; + } + }); + + if ($location->count() > 0) { + $locationId = $location->keys()->first(); + } + } + + return $locationId; + } + + /** + * Get the base dn for the query. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @return string + */ + private function getBaseDn(): string + { + if (!is_null($this->baseDn)) { + return $this->baseDn; + } + + return $this->ldapSettings['ldap_basedn']; + } + + /** + * Format the ldap filter if needed. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @return null|string + */ + private function getFilter(): ?string + { + $filter = $this->ldapSettings['ldap_filter']; + if ('' === $filter) { + return null; + } + // Add surrounding parentheses as needed + $paren = mb_substr($filter, 0, 1, 'utf-8'); + if ('(' !== $paren) { + return '('.$filter.')'; + } + + return $filter; + } + + /** + * Get the selected fields to return + * This should help with memory on large result sets as we are not returning all fields. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @return array + */ + private function getSelectedFields(): array + { + return [ + $this->ldapSettings['ldap_username_field'], + $this->ldapSettings['ldap_fname_field'], + $this->ldapSettings['ldap_lname_field'], + $this->ldapSettings['ldap_email'], + $this->ldapSettings['ldap_emp_num'], + 'memberOf', + 'useraccountcontrol', + ]; + } + + /** + * Test the bind user connection. + * + * @author Wes Hulette + * + * @since 5.0.0 + */ + public function testLdapAdBindConnection(): void + { + try { + $this->ldap->search()->ous()->get()->count(); + } catch (Exception $th) { + Log::error($th->getMessage()); + throw new Exception('Unable to search LDAP directory!'); + } + } + + /** + * Test the user can connect to the LDAP server. + * + * @author Wes Hulette + * + * @since 5.0.0 + */ + public function testLdapAdUserConnection(): void + { + try { + $this->ldap->connect(); + } catch (\Adldap\Auth\BindException $e) { + Log::error($e); + throw new Exception('Unable to connect to LDAP directory!'); + } + } + + /** + * Test the LDAP configuration by returning up to 10 users. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @return Collection + */ + public function testUserImportSync(): Collection + { + $testUsers = collect($this->getLdapUsers()->getResults())->chunk(10)->first(); + if ($testUsers) { + return $testUsers->map(function ($item) { + return (object) [ + 'username' => $item->{$this->ldapSettings['ldap_username_field']}[0] ?? null, + 'employee_number' => $item->{$this->ldapSettings['ldap_emp_num']}[0] ?? null, + 'lastname' => $item->{$this->ldapSettings['ldap_lname_field']}[0] ?? null, + 'firstname' => $item->{$this->ldapSettings['ldap_fname_field']}[0] ?? null, + 'email' => $item->{$this->ldapSettings['ldap_email']}[0] ?? null, + ]; + }); + } + + return collect(); + } + + /** + * Query the LDAP server to get the users to process and return a page set. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @param int $page The paged results to get + * + * @return \Adldap\Query\Paginator + */ + public function getLdapUsers(int $page=0): Paginator + { + $search = $this->ldap->search()->users()->in($this->getBaseDn()); + + $filter = $this->getFilter(); + if (!is_null($filter)) { + $search = $search->rawFilter($filter); + } + + return $search->select($this->getSelectedFields()) + ->paginate(self::PAGE_SIZE, $page); + } +} diff --git a/app/Models/LdapAdConfiguration.php b/app/Models/LdapAdConfiguration.php new file mode 100644 index 0000000000..22cee10da3 --- /dev/null +++ b/app/Models/LdapAdConfiguration.php @@ -0,0 +1,245 @@ + + * + * @since 5.0.0 + */ +class LdapAdConfiguration +{ + const LDAP_PORT = 389; + const CONNECTION_TIMEOUT = 5; + const DEFAULT_LDAP_VERSION = 3; + const LDAP_BOOLEAN_SETTINGS = ['ldap_enabled', 'ldap_server_cert_ignore', 'ldap_active_flag', 'ldap_tls', 'ldap_tls', 'ldap_pw_sync', 'is_ad']; + + /** + * Ldap Settings. + * + * @var Collection + */ + public $ldapSettings; + + /** + * LDAP Config. + * + * @var array + */ + public $ldapConfig; + + /** + * __construct. + */ + public function __construct() + { + $this->ldapSettings = $this->getSnipeItLdapSettings(); + $this->setSnipeItConfig(); + } + + /** + * Merge the default Adlap config with the SnipeIT config. + * + * @author Wes Hulette + * + * @since 5.0.0 + */ + private function setSnipeItConfig() + { + $this->ldapConfig = $this->setLdapConnectionConfiguration(); + $this->certificateCheck(); + } + + /** + * Get the LDAP settings from the Settings model. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @return \Illuminate\Support\Collection + */ + private function getSnipeItLdapSettings(): Collection + { + $ldapSettings = Setting::getLdapSettings() + ->map(function ($item, $key) { + // Trim the items + if (is_string($item)) { + $item = trim($item); + } + // Get the boolean value of the LDAP setting, makes it easier to work with them + if (in_array($key, self::LDAP_BOOLEAN_SETTINGS)) { + return boolval($item); + } + // Decrypt the admin password + if ('ldap_pword' === $key) { + try { + return decrypt($item); + } catch (Exception $e) { + throw new Exception('Your app key has changed! Could not decrypt LDAP password using your current app key, so LDAP authentication has been disabled. Login with a local account, update the LDAP password and re-enable it in Admin > Settings.'); + } + } + + return $item; + }); + + return $ldapSettings; + } + + /** + * Set the server certificate environment variable. + * + * @author Wes Hulette + * + * @since 5.0.0 + */ + private function certificateCheck(): void + { + // If we are ignoring the SSL cert we need to setup the environment variable + // before we create the connection + if ($this->ldapSettings['ldap_server_cert_ignore']) { + putenv('LDAPTLS_REQCERT=never'); + } + + // If the user specifies where CA Certs are, make sure to use them + if (env('LDAPTLS_CACERT')) { + putenv('LDAPTLS_CACERT='.env('LDAPTLS_CACERT')); + } + } + + /** + * Set the Adlap2 connection configuration values based on SnipeIT settings. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @return array + */ + private function setLdapConnectionConfiguration(): array + { + // Create the configuration array. + return [ + // Mandatory Configuration Options + 'hosts' => $this->getServerUrlBase(), + 'base_dn' => $this->ldapSettings['ldap_basedn'], + 'username' => $this->ldapSettings['ldap_uname'], + 'password' => $this->ldapSettings['ldap_pword'], + + // Optional Configuration Options + 'schema' => $this->getSchema(), + 'account_prefix' => '', + 'account_suffix' => '', + 'port' => $this->getPort(), + 'follow_referrals' => false, + 'use_ssl' => $this->isSsl(), + 'use_tls' => $this->ldapSettings['ldap_tls'], + 'version' => $this->ldapSettings['ldap_version'] ?? self::DEFAULT_LDAP_VERSION, + 'timeout' => self::CONNECTION_TIMEOUT, + + // Custom LDAP Options + 'custom_options' => [ + // See: http://php.net/ldap_set_option + // LDAP_OPT_X_TLS_REQUIRE_CERT => LDAP_OPT_X_TLS_HARD, + ], + ]; + } + + /** + * Get the schema to use for the connection. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @return string + */ + private function getSchema(): string + { + $schema = \Adldap\Schemas\OpenLDAP::class; + if ($this->ldapSettings['is_ad']) { + $schema = \Adldap\Schemas\ActiveDirectory::class; + } + + return $schema; + } + + /** + * Get the port number from the connection url. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @return int + */ + private function getPort(): int + { + $ldapUrl = $this->ldapSettings['ldap_server']; + if ($ldapUrl) { + $port = parse_url($ldapUrl, PHP_URL_PORT); + + if (is_int($port)) { + return $port; + } + } + + return self::LDAP_PORT; + } + + /** + * Get ldap scheme from url to determin ssl use. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @return bool + */ + private function isSsl(): bool + { + if ($this->ldapSettings['ldap_server']) { + $scheme = explode('://', $this->ldapSettings['ldap_server']); + if ('ldap' === strtolower($scheme[0])) { + return false; + } + + return true; + } + + return false; + } + + /** + * Return the base url to the LDAP server. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @return array + */ + private function getServerUrlBase(): array + { + if ($this->ldapSettings['is_ad']) { + return collect(explode(',', $this->ldapSettings['ad_domain']))->map(function ($item) { + return trim($item); + })->toArray(); + } + + $parts = explode('//', $this->ldapSettings['ldap_server']); + + return [ + $parts[1], + ]; + } +} diff --git a/app/Models/Setting.php b/app/Models/Setting.php index 77d72d4138..6301da3bee 100755 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -9,6 +9,9 @@ use Illuminate\Support\Facades\Cache; use Watson\Validating\ValidatingTrait; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; +use Watson\Validating\ValidatingTrait; +use Schema; +use Illuminate\Support\Collection; /** * Settings model. @@ -314,4 +317,43 @@ class Setting extends Model return 'required|min:'.$settings->pwd_secure_min.$security_rules; } + + + /** + * Get the specific LDAP settings + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @return Collection + */ + public static function getLdapSettings(): Collection + { + $ldapSettings = self::select([ + 'ldap_enabled', + 'ldap_server', + 'ldap_uname', + 'ldap_pword', + 'ldap_basedn', + 'ldap_filter', + 'ldap_username_field', + 'ldap_lname_field', + 'ldap_fname_field', + 'ldap_auth_filter_query', + 'ldap_version', + 'ldap_active_flag', + 'ldap_emp_num', + 'ldap_email', + 'ldap_server_cert_ignore', + 'ldap_port', + 'ldap_tls', + 'ldap_pw_sync', + 'is_ad', + 'ad_domain' + ])->first()->getAttributes(); + + return collect($ldapSettings); + } + } diff --git a/app/Traits/UserTrait.php b/app/Traits/UserTrait.php new file mode 100644 index 0000000000..9bc6d92fc2 --- /dev/null +++ b/app/Traits/UserTrait.php @@ -0,0 +1,36 @@ + + * + * @since 5.0.0 + * + * @return string + */ + public function generateEncyrptedPassword(): string + { + return bcrypt($this->generateUnencryptedPassword()); + } + + /** + * Get a random unencrypted password. + * + * @author Wes Hulette + * + * @since 5.0.0 + * + * @return string + */ + public function generateUnencryptedPassword(): string + { + return substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 20); + } +} diff --git a/composer.json b/composer.json index f0d2716e75..9f727edb18 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "type": "project", "require": { "php": ">=7.1.3", + "adldap2/adldap2": "^9.1", "bacon/bacon-qr-code": "^1.0", "doctrine/cache": "^1.6", "doctrine/common": "^2.7", diff --git a/composer.lock b/composer.lock index ac35dfbf6b..87aef7a87a 100644 --- a/composer.lock +++ b/composer.lock @@ -1,11 +1,64 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f80ee6df7f370344f1ff4e30bc08284d", + "content-hash": "fac7e8b237f1d78fb4057766c2c47da5", "packages": [ + { + "name": "adldap2/adldap2", + "version": "v9.1.3", + "source": { + "type": "git", + "url": "https://github.com/Adldap2/Adldap2.git", + "reference": "f9ba4003a1350b7cad952d3ad22b24c8d3e0d1af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Adldap2/Adldap2/zipball/f9ba4003a1350b7cad952d3ad22b24c8d3e0d1af", + "reference": "f9ba4003a1350b7cad952d3ad22b24c8d3e0d1af", + "shasum": "" + }, + "require": { + "ext-ldap": "*", + "illuminate/contracts": "~5.0", + "php": ">=7.0", + "tightenco/collect": "~5.0" + }, + "require-dev": { + "mockery/mockery": "~1.0", + "phpunit/phpunit": "~6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Adldap\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Steve Bauman", + "email": "steven_bauman@outlook.com", + "role": "Developer" + } + ], + "description": "A PHP LDAP Package for humans.", + "keywords": [ + "active directory", + "ad", + "adLDAP", + "adldap2", + "directory", + "ldap", + "windows" + ], + "time": "2018-10-08T22:25:07+00:00" + }, { "name": "aws/aws-sdk-php", "version": "3.69.0", @@ -5980,6 +6033,56 @@ ], "time": "2018-06-23T09:21:30+00:00" }, + { + "name": "tightenco/collect", + "version": "v5.7.9", + "source": { + "type": "git", + "url": "https://github.com/tightenco/collect.git", + "reference": "67b8d4a20ce42b32b5f50a141bb0b1ec45aedb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tightenco/collect/zipball/67b8d4a20ce42b32b5f50a141bb0b1ec45aedb53", + "reference": "67b8d4a20ce42b32b5f50a141bb0b1ec45aedb53", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/var-dumper": ">=3.4 <5" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "nesbot/carbon": "^1.26.3", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Collect/Support/helpers.php", + "src/Collect/Support/alias.php" + ], + "psr-4": { + "Tightenco\\Collect\\": "src/Collect" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylorotwell@gmail.com" + } + ], + "description": "Collect - Illuminate Collections as a separate package.", + "keywords": [ + "collection", + "laravel" + ], + "time": "2018-10-09T17:51:58+00:00" + }, { "name": "tightenco/ziggy", "version": "v0.6.8.1", @@ -7416,17 +7519,6 @@ { "name": "roave/security-advisories", "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "67643fa62d521fb76855b0a4cc9a3de7ba38f85b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/67643fa62d521fb76855b0a4cc9a3de7ba38f85b", - "reference": "67643fa62d521fb76855b0a4cc9a3de7ba38f85b", - "shasum": "" - }, "conflict": { "3f/pygmentize": "<1.2", "adodb/adodb-php": "<5.20.12", diff --git a/public/js/app.js b/public/js/app.js index 537aa79e17..57e00e0a94 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1 +1 @@ -!function(t){var e={};function n(r){if(e[r])return e[r].exports;var i=e[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:r})},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="/",n(n.s=0)}({"+sje":function(t,e,n){var r=n("VU/8")(n("eOaq"),n("lafA"),!1,function(t){n("4UNm")},"data-v-3bdd24a5",null);t.exports=r.exports},0:function(t,e,n){n("GDnL"),n("hw3k"),n("aKHO"),n("Bqz+"),t.exports=n("1CH1")},"0DKT":function(t,e){t.exports={render:function(){var t=this.$createElement;return(this._self._c||t)("select",{staticStyle:{width:"100%"}},[this._t("default")],2)},staticRenderFns:[]}},1:function(t,e){},"1CH1":function(t,e){},"1t9x":function(t,e,n){var r=n("wD+l");"string"==typeof r&&(r=[[t.i,r,""]]),r.locals&&(t.exports=r.locals);n("rjj0")("6522937c",r,!0,{})},"20cu":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.default={props:["clientsUrl","tokensUrl"],data:function(){return{tokens:[]}},ready:function(){this.prepareComponent()},mounted:function(){this.prepareComponent()},methods:{prepareComponent:function(){this.getTokens()},getTokens:function(){var t=this;this.$http.get(this.tokensUrl).then(function(e){t.tokens=e.data})},revoke:function(t){var e=this;this.$http.delete(this.tokensUrl+"/"+t.id).then(function(t){e.getTokens()})}}}},"265i":function(t,e,n){(t.exports=n("FZ+f")(!1)).push([t.i,"",""])},"3IRH":function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children||(t.children=[]),Object.defineProperty(t,"loaded",{enumerable:!0,get:function(){return t.l}}),Object.defineProperty(t,"id",{enumerable:!0,get:function(){return t.i}}),t.webpackPolyfill=1),t}},"4UNm":function(t,e,n){var r=n("HB0T");"string"==typeof r&&(r=[[t.i,r,""]]),r.locals&&(t.exports=r.locals);n("rjj0")("5d321c9c",r,!0,{})},"5F58":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),n("FHXl"),e.default={data:function(){return{files:[],displayImportModal:!1,activeFile:null,alert:{type:null,message:null,visible:!1},importErrors:null,progress:{currentClass:"progress-bar-warning",currentPercent:"0",statusText:"",visible:!1},customFields:[]}},mounted:function(){window.eventHub.$on("importErrors",this.updateImportErrors),this.fetchFiles(),this.fetchCustomFields();var t=this;$("#fileupload").fileupload({dataType:"json",done:function(e,n){t.progress.currentClass="progress-bar-success",t.progress.statusText="Success!",t.files=n.result.files.concat(t.files),console.log(n.result.header_row)},add:function(e,n){n.headers={"X-Requested-With":"XMLHttpRequest","X-CSRF-TOKEN":Laravel.csrfToken},n.process().done(function(){n.submit()}),t.progress.visible=!0},progress:function(e,n){var r=parseInt((n.loaded,n.total,10));t.progress.currentPercent=r,t.progress.statusText=r+"% Complete"},fail:function(e,n){t.progress.currentClass="progress-bar-danger",t.progress.statusText=n.jqXHR.responseJSON.messages}})},methods:{fetchFiles:function(){var t=this;this.$http.get(route("api.imports.index")).then(function(e){var n=e.data;return t.files=n},function(e){t.alert.type="danger",t.alert.visible=!0,t.alert.message="Something went wrong fetching files..."})},fetchCustomFields:function(){var t=this;this.$http.get(route("api.customfields.index")).then(function(e){var n=e.data;(n=n.rows).forEach(function(e){t.customFields.push({id:e.db_column_name,text:e.name})})})},deleteFile:function(t,e){var n=this;this.$http.delete(route("api.imports.destroy",t.id)).then(function(t){n.files.splice(e,1),n.alert.type=t.body.status,n.alert.visible=!0,n.alert.message=t.body.messages},function(t){n.alert.type="error",n.alert.visible=!0,n.alert.message=t.body.messages})},toggleEvent:function(t){window.eventHub.$emit("showDetails",t)},updateAlert:function(t){this.alert=t},updateImportErrors:function(t){this.importErrors=t}},computed:{progressWidth:function(){return"width: "+10*this.progress.currentPercent+"%"}},components:{alert:n("b7rt"),errors:n("yDVJ"),importFile:n("sJcK")}}},"5m3O":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};e.default={props:["clientsUrl"],data:function(){return{clients:[],createForm:{errors:[],name:"",redirect:""},editForm:{errors:[],name:"",redirect:""}}},ready:function(){this.prepareComponent()},mounted:function(){this.prepareComponent()},methods:{prepareComponent:function(){this.getClients(),$("#modal-create-client").on("shown.bs.modal",function(){$("#create-client-name").focus()}),$("#modal-edit-client").on("shown.bs.modal",function(){$("#edit-client-name").focus()})},getClients:function(){var t=this;this.$http.get(this.clientsUrl).then(function(e){t.clients=e.data})},showCreateClientForm:function(){$("#modal-create-client").modal("show")},store:function(){this.persistClient("post",this.clientsUrl,this.createForm,"#modal-create-client")},edit:function(t){this.editForm.id=t.id,this.editForm.name=t.name,this.editForm.redirect=t.redirect,$("#modal-edit-client").modal("show")},update:function(){this.persistClient("put",this.clientsUrl+"/"+this.editForm.id,this.editForm,"#modal-edit-client")},persistClient:function(t,e,n,i){var o=this;console.log("persisting"),n.errors=[],console.log("method: "+t),this.$http[t](e,n).then(function(t){o.getClients(),n.name="",n.redirect="",n.errors=[],$(i).modal("hide")}).catch(function(t){"object"===r(t.data)?n.errors=_.flatten(_.toArray(t.data)):n.errors=["Something went wrong. Please try again."]})},destroy:function(t){var e=this;this.$http.delete(this.clientsUrl+"/"+t.id).then(function(t){e.getClients()})}}}},"7t+N":function(t,e,n){var r;!function(e,n){"use strict";"object"==typeof t&&"object"==typeof t.exports?t.exports=e.document?n(e,!0):function(t){if(!t.document)throw new Error("jQuery requires a window with a document");return n(t)}:n(e)}("undefined"!=typeof window?window:this,function(n,i){"use strict";var o=[],s=n.document,a=Object.getPrototypeOf,l=o.slice,u=o.concat,c=o.push,f=o.indexOf,d={},p=d.toString,h=d.hasOwnProperty,v=h.toString,g=v.call(Object),m={},y=function(t){return"function"==typeof t&&"number"!=typeof t.nodeType},_=function(t){return null!=t&&t===t.window},b={type:!0,src:!0,noModule:!0};function w(t,e,n){var r,i=(e=e||s).createElement("script");if(i.text=t,n)for(r in b)n[r]&&(i[r]=n[r]);e.head.appendChild(i).parentNode.removeChild(i)}function x(t){return null==t?t+"":"object"==typeof t||"function"==typeof t?d[p.call(t)]||"object":typeof t}var C=function(t,e){return new C.fn.init(t,e)},$=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function T(t){var e=!!t&&"length"in t&&t.length,n=x(t);return!y(t)&&!_(t)&&("array"===n||0===e||"number"==typeof e&&e>0&&e-1 in t)}C.fn=C.prototype={jquery:"3.3.1",constructor:C,length:0,toArray:function(){return l.call(this)},get:function(t){return null==t?l.call(this):t<0?this[t+this.length]:this[t]},pushStack:function(t){var e=C.merge(this.constructor(),t);return e.prevObject=this,e},each:function(t){return C.each(this,t)},map:function(t){return this.pushStack(C.map(this,function(e,n){return t.call(e,n,e)}))},slice:function(){return this.pushStack(l.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(t){var e=this.length,n=+t+(t<0?e:0);return this.pushStack(n>=0&&n+~]|"+L+")"+L+"*"),z=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),W=new RegExp(M),V=new RegExp("^"+P+"$"),X={ID:new RegExp("^#("+P+")"),CLASS:new RegExp("^\\.("+P+")"),TAG:new RegExp("^("+P+"|[*])"),ATTR:new RegExp("^"+F),PSEUDO:new RegExp("^"+M),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},K=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,G=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Q=/[+~]/,Y=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),tt=function(t,e,n){var r="0x"+e-65536;return r!=r||n?e:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},et=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,nt=function(t,e){return e?"\0"===t?"�":t.slice(0,-1)+"\\"+t.charCodeAt(t.length-1).toString(16)+" ":"\\"+t},rt=function(){d()},it=yt(function(t){return!0===t.disabled&&("form"in t||"label"in t)},{dir:"parentNode",next:"legend"});try{D.apply(S=N.call(w.childNodes),w.childNodes),S[w.childNodes.length].nodeType}catch(t){D={apply:S.length?function(t,e){j.apply(t,N.call(e))}:function(t,e){for(var n=t.length,r=0;t[n++]=e[r++];);t.length=n-1}}}function ot(t,e,r,i){var o,a,u,c,f,h,m,y=e&&e.ownerDocument,x=e?e.nodeType:9;if(r=r||[],"string"!=typeof t||!t||1!==x&&9!==x&&11!==x)return r;if(!i&&((e?e.ownerDocument||e:w)!==p&&d(e),e=e||p,v)){if(11!==x&&(f=J.exec(t)))if(o=f[1]){if(9===x){if(!(u=e.getElementById(o)))return r;if(u.id===o)return r.push(u),r}else if(y&&(u=y.getElementById(o))&&_(e,u)&&u.id===o)return r.push(u),r}else{if(f[2])return D.apply(r,e.getElementsByTagName(t)),r;if((o=f[3])&&n.getElementsByClassName&&e.getElementsByClassName)return D.apply(r,e.getElementsByClassName(o)),r}if(n.qsa&&!k[t+" "]&&(!g||!g.test(t))){if(1!==x)y=e,m=t;else if("object"!==e.nodeName.toLowerCase()){for((c=e.getAttribute("id"))?c=c.replace(et,nt):e.setAttribute("id",c=b),a=(h=s(t)).length;a--;)h[a]="#"+c+" "+mt(h[a]);m=h.join(","),y=Q.test(t)&&vt(e.parentNode)||e}if(m)try{return D.apply(r,y.querySelectorAll(m)),r}catch(t){}finally{c===b&&e.removeAttribute("id")}}}return l(t.replace(q,"$1"),e,r,i)}function st(){var t=[];return function e(n,i){return t.push(n+" ")>r.cacheLength&&delete e[t.shift()],e[n+" "]=i}}function at(t){return t[b]=!0,t}function lt(t){var e=p.createElement("fieldset");try{return!!t(e)}catch(t){return!1}finally{e.parentNode&&e.parentNode.removeChild(e),e=null}}function ut(t,e){for(var n=t.split("|"),i=n.length;i--;)r.attrHandle[n[i]]=e}function ct(t,e){var n=e&&t,r=n&&1===t.nodeType&&1===e.nodeType&&t.sourceIndex-e.sourceIndex;if(r)return r;if(n)for(;n=n.nextSibling;)if(n===e)return-1;return t?1:-1}function ft(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function dt(t){return function(e){var n=e.nodeName.toLowerCase();return("input"===n||"button"===n)&&e.type===t}}function pt(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&it(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ht(t){return at(function(e){return e=+e,at(function(n,r){for(var i,o=t([],n.length,e),s=o.length;s--;)n[i=o[s]]&&(n[i]=!(r[i]=n[i]))})})}function vt(t){return t&&void 0!==t.getElementsByTagName&&t}for(e in n=ot.support={},o=ot.isXML=function(t){var e=t&&(t.ownerDocument||t).documentElement;return!!e&&"HTML"!==e.nodeName},d=ot.setDocument=function(t){var e,i,s=t?t.ownerDocument||t:w;return s!==p&&9===s.nodeType&&s.documentElement?(h=(p=s).documentElement,v=!o(p),w!==p&&(i=p.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",rt,!1):i.attachEvent&&i.attachEvent("onunload",rt)),n.attributes=lt(function(t){return t.className="i",!t.getAttribute("className")}),n.getElementsByTagName=lt(function(t){return t.appendChild(p.createComment("")),!t.getElementsByTagName("*").length}),n.getElementsByClassName=G.test(p.getElementsByClassName),n.getById=lt(function(t){return h.appendChild(t).id=b,!p.getElementsByName||!p.getElementsByName(b).length}),n.getById?(r.filter.ID=function(t){var e=t.replace(Y,tt);return function(t){return t.getAttribute("id")===e}},r.find.ID=function(t,e){if(void 0!==e.getElementById&&v){var n=e.getElementById(t);return n?[n]:[]}}):(r.filter.ID=function(t){var e=t.replace(Y,tt);return function(t){var n=void 0!==t.getAttributeNode&&t.getAttributeNode("id");return n&&n.value===e}},r.find.ID=function(t,e){if(void 0!==e.getElementById&&v){var n,r,i,o=e.getElementById(t);if(o){if((n=o.getAttributeNode("id"))&&n.value===t)return[o];for(i=e.getElementsByName(t),r=0;o=i[r++];)if((n=o.getAttributeNode("id"))&&n.value===t)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(t,e){return void 0!==e.getElementsByTagName?e.getElementsByTagName(t):n.qsa?e.querySelectorAll(t):void 0}:function(t,e){var n,r=[],i=0,o=e.getElementsByTagName(t);if("*"===t){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(t,e){if(void 0!==e.getElementsByClassName&&v)return e.getElementsByClassName(t)},m=[],g=[],(n.qsa=G.test(p.querySelectorAll))&&(lt(function(t){h.appendChild(t).innerHTML="",t.querySelectorAll("[msallowcapture^='']").length&&g.push("[*^$]="+L+"*(?:''|\"\")"),t.querySelectorAll("[selected]").length||g.push("\\["+L+"*(?:value|"+R+")"),t.querySelectorAll("[id~="+b+"-]").length||g.push("~="),t.querySelectorAll(":checked").length||g.push(":checked"),t.querySelectorAll("a#"+b+"+*").length||g.push(".#.+[+~]")}),lt(function(t){t.innerHTML="";var e=p.createElement("input");e.setAttribute("type","hidden"),t.appendChild(e).setAttribute("name","D"),t.querySelectorAll("[name=d]").length&&g.push("name"+L+"*[*^$|!~]?="),2!==t.querySelectorAll(":enabled").length&&g.push(":enabled",":disabled"),h.appendChild(t).disabled=!0,2!==t.querySelectorAll(":disabled").length&&g.push(":enabled",":disabled"),t.querySelectorAll("*,:x"),g.push(",.*:")})),(n.matchesSelector=G.test(y=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&<(function(t){n.disconnectedMatch=y.call(t,"*"),y.call(t,"[s!='']:x"),m.push("!=",M)}),g=g.length&&new RegExp(g.join("|")),m=m.length&&new RegExp(m.join("|")),e=G.test(h.compareDocumentPosition),_=e||G.test(h.contains)?function(t,e){var n=9===t.nodeType?t.documentElement:t,r=e&&e.parentNode;return t===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):t.compareDocumentPosition&&16&t.compareDocumentPosition(r)))}:function(t,e){if(e)for(;e=e.parentNode;)if(e===t)return!0;return!1},A=e?function(t,e){if(t===e)return f=!0,0;var r=!t.compareDocumentPosition-!e.compareDocumentPosition;return r||(1&(r=(t.ownerDocument||t)===(e.ownerDocument||e)?t.compareDocumentPosition(e):1)||!n.sortDetached&&e.compareDocumentPosition(t)===r?t===p||t.ownerDocument===w&&_(w,t)?-1:e===p||e.ownerDocument===w&&_(w,e)?1:c?I(c,t)-I(c,e):0:4&r?-1:1)}:function(t,e){if(t===e)return f=!0,0;var n,r=0,i=t.parentNode,o=e.parentNode,s=[t],a=[e];if(!i||!o)return t===p?-1:e===p?1:i?-1:o?1:c?I(c,t)-I(c,e):0;if(i===o)return ct(t,e);for(n=t;n=n.parentNode;)s.unshift(n);for(n=e;n=n.parentNode;)a.unshift(n);for(;s[r]===a[r];)r++;return r?ct(s[r],a[r]):s[r]===w?-1:a[r]===w?1:0},p):p},ot.matches=function(t,e){return ot(t,null,null,e)},ot.matchesSelector=function(t,e){if((t.ownerDocument||t)!==p&&d(t),e=e.replace(z,"='$1']"),n.matchesSelector&&v&&!k[e+" "]&&(!m||!m.test(e))&&(!g||!g.test(e)))try{var r=y.call(t,e);if(r||n.disconnectedMatch||t.document&&11!==t.document.nodeType)return r}catch(t){}return ot(e,p,null,[t]).length>0},ot.contains=function(t,e){return(t.ownerDocument||t)!==p&&d(t),_(t,e)},ot.attr=function(t,e){(t.ownerDocument||t)!==p&&d(t);var i=r.attrHandle[e.toLowerCase()],o=i&&E.call(r.attrHandle,e.toLowerCase())?i(t,e,!v):void 0;return void 0!==o?o:n.attributes||!v?t.getAttribute(e):(o=t.getAttributeNode(e))&&o.specified?o.value:null},ot.escape=function(t){return(t+"").replace(et,nt)},ot.error=function(t){throw new Error("Syntax error, unrecognized expression: "+t)},ot.uniqueSort=function(t){var e,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&t.slice(0),t.sort(A),f){for(;e=t[o++];)e===t[o]&&(i=r.push(o));for(;i--;)t.splice(r[i],1)}return c=null,t},i=ot.getText=function(t){var e,n="",r=0,o=t.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof t.textContent)return t.textContent;for(t=t.firstChild;t;t=t.nextSibling)n+=i(t)}else if(3===o||4===o)return t.nodeValue}else for(;e=t[r++];)n+=i(e);return n},(r=ot.selectors={cacheLength:50,createPseudo:at,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(t){return t[1]=t[1].replace(Y,tt),t[3]=(t[3]||t[4]||t[5]||"").replace(Y,tt),"~="===t[2]&&(t[3]=" "+t[3]+" "),t.slice(0,4)},CHILD:function(t){return t[1]=t[1].toLowerCase(),"nth"===t[1].slice(0,3)?(t[3]||ot.error(t[0]),t[4]=+(t[4]?t[5]+(t[6]||1):2*("even"===t[3]||"odd"===t[3])),t[5]=+(t[7]+t[8]||"odd"===t[3])):t[3]&&ot.error(t[0]),t},PSEUDO:function(t){var e,n=!t[6]&&t[2];return X.CHILD.test(t[0])?null:(t[3]?t[2]=t[4]||t[5]||"":n&&W.test(n)&&(e=s(n,!0))&&(e=n.indexOf(")",n.length-e)-n.length)&&(t[0]=t[0].slice(0,e),t[2]=n.slice(0,e)),t.slice(0,3))}},filter:{TAG:function(t){var e=t.replace(Y,tt).toLowerCase();return"*"===t?function(){return!0}:function(t){return t.nodeName&&t.nodeName.toLowerCase()===e}},CLASS:function(t){var e=$[t+" "];return e||(e=new RegExp("(^|"+L+")"+t+"("+L+"|$)"))&&$(t,function(t){return e.test("string"==typeof t.className&&t.className||void 0!==t.getAttribute&&t.getAttribute("class")||"")})},ATTR:function(t,e,n){return function(r){var i=ot.attr(r,t);return null==i?"!="===e:!e||(i+="","="===e?i===n:"!="===e?i!==n:"^="===e?n&&0===i.indexOf(n):"*="===e?n&&i.indexOf(n)>-1:"$="===e?n&&i.slice(-n.length)===n:"~="===e?(" "+i.replace(U," ")+" ").indexOf(n)>-1:"|="===e&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(t,e,n,r,i){var o="nth"!==t.slice(0,3),s="last"!==t.slice(-4),a="of-type"===e;return 1===r&&0===i?function(t){return!!t.parentNode}:function(e,n,l){var u,c,f,d,p,h,v=o!==s?"nextSibling":"previousSibling",g=e.parentNode,m=a&&e.nodeName.toLowerCase(),y=!l&&!a,_=!1;if(g){if(o){for(;v;){for(d=e;d=d[v];)if(a?d.nodeName.toLowerCase()===m:1===d.nodeType)return!1;h=v="only"===t&&!h&&"nextSibling"}return!0}if(h=[s?g.firstChild:g.lastChild],s&&y){for(_=(p=(u=(c=(f=(d=g)[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[t]||[])[0]===x&&u[1])&&u[2],d=p&&g.childNodes[p];d=++p&&d&&d[v]||(_=p=0)||h.pop();)if(1===d.nodeType&&++_&&d===e){c[t]=[x,p,_];break}}else if(y&&(_=p=(u=(c=(f=(d=e)[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[t]||[])[0]===x&&u[1]),!1===_)for(;(d=++p&&d&&d[v]||(_=p=0)||h.pop())&&((a?d.nodeName.toLowerCase()!==m:1!==d.nodeType)||!++_||(y&&((c=(f=d[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[t]=[x,_]),d!==e)););return(_-=i)===r||_%r==0&&_/r>=0}}},PSEUDO:function(t,e){var n,i=r.pseudos[t]||r.setFilters[t.toLowerCase()]||ot.error("unsupported pseudo: "+t);return i[b]?i(e):i.length>1?(n=[t,t,"",e],r.setFilters.hasOwnProperty(t.toLowerCase())?at(function(t,n){for(var r,o=i(t,e),s=o.length;s--;)t[r=I(t,o[s])]=!(n[r]=o[s])}):function(t){return i(t,0,n)}):i}},pseudos:{not:at(function(t){var e=[],n=[],r=a(t.replace(q,"$1"));return r[b]?at(function(t,e,n,i){for(var o,s=r(t,null,i,[]),a=t.length;a--;)(o=s[a])&&(t[a]=!(e[a]=o))}):function(t,i,o){return e[0]=t,r(e,null,o,n),e[0]=null,!n.pop()}}),has:at(function(t){return function(e){return ot(t,e).length>0}}),contains:at(function(t){return t=t.replace(Y,tt),function(e){return(e.textContent||e.innerText||i(e)).indexOf(t)>-1}}),lang:at(function(t){return V.test(t||"")||ot.error("unsupported lang: "+t),t=t.replace(Y,tt).toLowerCase(),function(e){var n;do{if(n=v?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(n=n.toLowerCase())===t||0===n.indexOf(t+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var n=t.location&&t.location.hash;return n&&n.slice(1)===e.id},root:function(t){return t===h},focus:function(t){return t===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(t.type||t.href||~t.tabIndex)},enabled:pt(!1),disabled:pt(!0),checked:function(t){var e=t.nodeName.toLowerCase();return"input"===e&&!!t.checked||"option"===e&&!!t.selected},selected:function(t){return t.parentNode&&t.parentNode.selectedIndex,!0===t.selected},empty:function(t){for(t=t.firstChild;t;t=t.nextSibling)if(t.nodeType<6)return!1;return!0},parent:function(t){return!r.pseudos.empty(t)},header:function(t){return Z.test(t.nodeName)},input:function(t){return K.test(t.nodeName)},button:function(t){var e=t.nodeName.toLowerCase();return"input"===e&&"button"===t.type||"button"===e},text:function(t){var e;return"input"===t.nodeName.toLowerCase()&&"text"===t.type&&(null==(e=t.getAttribute("type"))||"text"===e.toLowerCase())},first:ht(function(){return[0]}),last:ht(function(t,e){return[e-1]}),eq:ht(function(t,e,n){return[n<0?n+e:n]}),even:ht(function(t,e){for(var n=0;n=0;)t.push(r);return t}),gt:ht(function(t,e,n){for(var r=n<0?n+e:n;++r1?function(e,n,r){for(var i=t.length;i--;)if(!t[i](e,n,r))return!1;return!0}:t[0]}function bt(t,e,n,r,i){for(var o,s=[],a=0,l=t.length,u=null!=e;a-1&&(o[u]=!(s[u]=f))}}else m=bt(m===s?m.splice(h,m.length):m),i?i(null,s,m,l):D.apply(s,m)})}function xt(t){for(var e,n,i,o=t.length,s=r.relative[t[0].type],a=s||r.relative[" "],l=s?1:0,c=yt(function(t){return t===e},a,!0),f=yt(function(t){return I(e,t)>-1},a,!0),d=[function(t,n,r){var i=!s&&(r||n!==u)||((e=n).nodeType?c(t,n,r):f(t,n,r));return e=null,i}];l1&&_t(d),l>1&&mt(t.slice(0,l-1).concat({value:" "===t[l-2].type?"*":""})).replace(q,"$1"),n,l0,i=t.length>0,o=function(o,s,a,l,c){var f,h,g,m=0,y="0",_=o&&[],b=[],w=u,C=o||i&&r.find.TAG("*",c),$=x+=null==w?1:Math.random()||.1,T=C.length;for(c&&(u=s===p||s||c);y!==T&&null!=(f=C[y]);y++){if(i&&f){for(h=0,s||f.ownerDocument===p||(d(f),a=!v);g=t[h++];)if(g(f,s||p,a)){l.push(f);break}c&&(x=$)}n&&((f=!g&&f)&&m--,o&&_.push(f))}if(m+=y,n&&y!==m){for(h=0;g=e[h++];)g(_,b,s,a);if(o){if(m>0)for(;y--;)_[y]||b[y]||(b[y]=O.call(l));b=bt(b)}D.apply(l,b),c&&!o&&b.length>0&&m+e.length>1&&ot.uniqueSort(l)}return c&&(x=$,u=w),_};return n?at(o):o}(o,i))).selector=t}return a},l=ot.select=function(t,e,n,i){var o,l,u,c,f,d="function"==typeof t&&t,p=!i&&s(t=d.selector||t);if(n=n||[],1===p.length){if((l=p[0]=p[0].slice(0)).length>2&&"ID"===(u=l[0]).type&&9===e.nodeType&&v&&r.relative[l[1].type]){if(!(e=(r.find.ID(u.matches[0].replace(Y,tt),e)||[])[0]))return n;d&&(e=e.parentNode),t=t.slice(l.shift().value.length)}for(o=X.needsContext.test(t)?0:l.length;o--&&(u=l[o],!r.relative[c=u.type]);)if((f=r.find[c])&&(i=f(u.matches[0].replace(Y,tt),Q.test(l[0].type)&&vt(e.parentNode)||e))){if(l.splice(o,1),!(t=i.length&&mt(l)))return D.apply(n,i),n;break}}return(d||a(t,p))(i,e,!v,n,!e||Q.test(t)&&vt(e.parentNode)||e),n},n.sortStable=b.split("").sort(A).join("")===b,n.detectDuplicates=!!f,d(),n.sortDetached=lt(function(t){return 1&t.compareDocumentPosition(p.createElement("fieldset"))}),lt(function(t){return t.innerHTML="","#"===t.firstChild.getAttribute("href")})||ut("type|href|height|width",function(t,e,n){if(!n)return t.getAttribute(e,"type"===e.toLowerCase()?1:2)}),n.attributes&<(function(t){return t.innerHTML="",t.firstChild.setAttribute("value",""),""===t.firstChild.getAttribute("value")})||ut("value",function(t,e,n){if(!n&&"input"===t.nodeName.toLowerCase())return t.defaultValue}),lt(function(t){return null==t.getAttribute("disabled")})||ut(R,function(t,e,n){var r;if(!n)return!0===t[e]?e.toLowerCase():(r=t.getAttributeNode(e))&&r.specified?r.value:null}),ot}(n);C.find=k,C.expr=k.selectors,C.expr[":"]=C.expr.pseudos,C.uniqueSort=C.unique=k.uniqueSort,C.text=k.getText,C.isXMLDoc=k.isXML,C.contains=k.contains,C.escapeSelector=k.escape;var A=function(t,e,n){for(var r=[],i=void 0!==n;(t=t[e])&&9!==t.nodeType;)if(1===t.nodeType){if(i&&C(t).is(n))break;r.push(t)}return r},E=function(t,e){for(var n=[];t;t=t.nextSibling)1===t.nodeType&&t!==e&&n.push(t);return n},S=C.expr.match.needsContext;function O(t,e){return t.nodeName&&t.nodeName.toLowerCase()===e.toLowerCase()}var j=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(t,e,n){return y(e)?C.grep(t,function(t,r){return!!e.call(t,r,t)!==n}):e.nodeType?C.grep(t,function(t){return t===e!==n}):"string"!=typeof e?C.grep(t,function(t){return f.call(e,t)>-1!==n}):C.filter(e,t,n)}C.filter=function(t,e,n){var r=e[0];return n&&(t=":not("+t+")"),1===e.length&&1===r.nodeType?C.find.matchesSelector(r,t)?[r]:[]:C.find.matches(t,C.grep(e,function(t){return 1===t.nodeType}))},C.fn.extend({find:function(t){var e,n,r=this.length,i=this;if("string"!=typeof t)return this.pushStack(C(t).filter(function(){for(e=0;e1?C.uniqueSort(n):n},filter:function(t){return this.pushStack(D(this,t||[],!1))},not:function(t){return this.pushStack(D(this,t||[],!0))},is:function(t){return!!D(this,"string"==typeof t&&S.test(t)?C(t):t||[],!1).length}});var N,I=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(C.fn.init=function(t,e,n){var r,i;if(!t)return this;if(n=n||N,"string"==typeof t){if(!(r="<"===t[0]&&">"===t[t.length-1]&&t.length>=3?[null,t,null]:I.exec(t))||!r[1]&&e)return!e||e.jquery?(e||n).find(t):this.constructor(e).find(t);if(r[1]){if(e=e instanceof C?e[0]:e,C.merge(this,C.parseHTML(r[1],e&&e.nodeType?e.ownerDocument||e:s,!0)),j.test(r[1])&&C.isPlainObject(e))for(r in e)y(this[r])?this[r](e[r]):this.attr(r,e[r]);return this}return(i=s.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return t.nodeType?(this[0]=t,this.length=1,this):y(t)?void 0!==n.ready?n.ready(t):t(C):C.makeArray(t,this)}).prototype=C.fn,N=C(s);var R=/^(?:parents|prev(?:Until|All))/,L={children:!0,contents:!0,next:!0,prev:!0};function P(t,e){for(;(t=t[e])&&1!==t.nodeType;);return t}C.fn.extend({has:function(t){var e=C(t,this),n=e.length;return this.filter(function(){for(var t=0;t-1:1===n.nodeType&&C.find.matchesSelector(n,t))){o.push(n);break}return this.pushStack(o.length>1?C.uniqueSort(o):o)},index:function(t){return t?"string"==typeof t?f.call(C(t),this[0]):f.call(this,t.jquery?t[0]:t):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(t,e){return this.pushStack(C.uniqueSort(C.merge(this.get(),C(t,e))))},addBack:function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}}),C.each({parent:function(t){var e=t.parentNode;return e&&11!==e.nodeType?e:null},parents:function(t){return A(t,"parentNode")},parentsUntil:function(t,e,n){return A(t,"parentNode",n)},next:function(t){return P(t,"nextSibling")},prev:function(t){return P(t,"previousSibling")},nextAll:function(t){return A(t,"nextSibling")},prevAll:function(t){return A(t,"previousSibling")},nextUntil:function(t,e,n){return A(t,"nextSibling",n)},prevUntil:function(t,e,n){return A(t,"previousSibling",n)},siblings:function(t){return E((t.parentNode||{}).firstChild,t)},children:function(t){return E(t.firstChild)},contents:function(t){return O(t,"iframe")?t.contentDocument:(O(t,"template")&&(t=t.content||t),C.merge([],t.childNodes))}},function(t,e){C.fn[t]=function(n,r){var i=C.map(this,e,n);return"Until"!==t.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=C.filter(r,i)),this.length>1&&(L[t]||C.uniqueSort(i),R.test(t)&&i.reverse()),this.pushStack(i)}});var F=/[^\x20\t\r\n\f]+/g;function M(t){return t}function U(t){throw t}function q(t,e,n,r){var i;try{t&&y(i=t.promise)?i.call(t).done(e).fail(n):t&&y(i=t.then)?i.call(t,e,n):e.apply(void 0,[t].slice(r))}catch(t){n.apply(void 0,[t])}}C.Callbacks=function(t){t="string"==typeof t?function(t){var e={};return C.each(t.match(F)||[],function(t,n){e[n]=!0}),e}(t):C.extend({},t);var e,n,r,i,o=[],s=[],a=-1,l=function(){for(i=i||t.once,r=e=!0;s.length;a=-1)for(n=s.shift();++a-1;)o.splice(n,1),n<=a&&a--}),this},has:function(t){return t?C.inArray(t,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=s=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=s=[],n||e||(o=n=""),this},locked:function(){return!!i},fireWith:function(t,n){return i||(n=[t,(n=n||[]).slice?n.slice():n],s.push(n),e||l()),this},fire:function(){return u.fireWith(this,arguments),this},fired:function(){return!!r}};return u},C.extend({Deferred:function(t){var e=[["notify","progress",C.Callbacks("memory"),C.Callbacks("memory"),2],["resolve","done",C.Callbacks("once memory"),C.Callbacks("once memory"),0,"resolved"],["reject","fail",C.Callbacks("once memory"),C.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},catch:function(t){return i.then(null,t)},pipe:function(){var t=arguments;return C.Deferred(function(n){C.each(e,function(e,r){var i=y(t[r[4]])&&t[r[4]];o[r[1]](function(){var t=i&&i.apply(this,arguments);t&&y(t.promise)?t.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[r[0]+"With"](this,i?[t]:arguments)})}),t=null}).promise()},then:function(t,r,i){var o=0;function s(t,e,r,i){return function(){var a=this,l=arguments,u=function(){var n,u;if(!(t=o&&(r!==U&&(a=void 0,l=[n]),e.rejectWith(a,l))}};t?c():(C.Deferred.getStackHook&&(c.stackTrace=C.Deferred.getStackHook()),n.setTimeout(c))}}return C.Deferred(function(n){e[0][3].add(s(0,n,y(i)?i:M,n.notifyWith)),e[1][3].add(s(0,n,y(t)?t:M)),e[2][3].add(s(0,n,y(r)?r:U))}).promise()},promise:function(t){return null!=t?C.extend(t,i):i}},o={};return C.each(e,function(t,n){var s=n[2],a=n[5];i[n[1]]=s.add,a&&s.add(function(){r=a},e[3-t][2].disable,e[3-t][3].disable,e[0][2].lock,e[0][3].lock),s.add(n[3].fire),o[n[0]]=function(){return o[n[0]+"With"](this===o?void 0:this,arguments),this},o[n[0]+"With"]=s.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(t){var e=arguments.length,n=e,r=Array(n),i=l.call(arguments),o=C.Deferred(),s=function(t){return function(n){r[t]=this,i[t]=arguments.length>1?l.call(arguments):n,--e||o.resolveWith(r,i)}};if(e<=1&&(q(t,o.done(s(n)).resolve,o.reject,!e),"pending"===o.state()||y(i[n]&&i[n].then)))return o.then();for(;n--;)q(i[n],s(n),o.reject);return o.promise()}});var H=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;C.Deferred.exceptionHook=function(t,e){n.console&&n.console.warn&&t&&H.test(t.name)&&n.console.warn("jQuery.Deferred exception: "+t.message,t.stack,e)},C.readyException=function(t){n.setTimeout(function(){throw t})};var B=C.Deferred();function z(){s.removeEventListener("DOMContentLoaded",z),n.removeEventListener("load",z),C.ready()}C.fn.ready=function(t){return B.then(t).catch(function(t){C.readyException(t)}),this},C.extend({isReady:!1,readyWait:1,ready:function(t){(!0===t?--C.readyWait:C.isReady)||(C.isReady=!0,!0!==t&&--C.readyWait>0||B.resolveWith(s,[C]))}}),C.ready.then=B.then,"complete"===s.readyState||"loading"!==s.readyState&&!s.documentElement.doScroll?n.setTimeout(C.ready):(s.addEventListener("DOMContentLoaded",z),n.addEventListener("load",z));var W=function(t,e,n,r,i,o,s){var a=0,l=t.length,u=null==n;if("object"===x(n))for(a in i=!0,n)W(t,e,a,n[a],!0,o,s);else if(void 0!==r&&(i=!0,y(r)||(s=!0),u&&(s?(e.call(t,r),e=null):(u=e,e=function(t,e,n){return u.call(C(t),n)})),e))for(;a1,null,!0)},removeData:function(t){return this.each(function(){Y.remove(this,t)})}}),C.extend({queue:function(t,e,n){var r;if(t)return e=(e||"fx")+"queue",r=Q.get(t,e),n&&(!r||Array.isArray(n)?r=Q.access(t,e,C.makeArray(n)):r.push(n)),r||[]},dequeue:function(t,e){e=e||"fx";var n=C.queue(t,e),r=n.length,i=n.shift(),o=C._queueHooks(t,e);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===e&&n.unshift("inprogress"),delete o.stop,i.call(t,function(){C.dequeue(t,e)},o)),!r&&o&&o.empty.fire()},_queueHooks:function(t,e){var n=e+"queueHooks";return Q.get(t,n)||Q.access(t,n,{empty:C.Callbacks("once memory").add(function(){Q.remove(t,[e+"queue",n])})})}}),C.fn.extend({queue:function(t,e){var n=2;return"string"!=typeof t&&(e=t,t="fx",n--),arguments.length\x20\t\r\n\f]+)/i,ht=/^$|^module$|\/(?:java|ecma)script/i,vt={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function gt(t,e){var n;return n=void 0!==t.getElementsByTagName?t.getElementsByTagName(e||"*"):void 0!==t.querySelectorAll?t.querySelectorAll(e||"*"):[],void 0===e||e&&O(t,e)?C.merge([t],n):n}function mt(t,e){for(var n=0,r=t.length;n-1)i&&i.push(o);else if(u=C.contains(o.ownerDocument,o),s=gt(f.appendChild(o),"script"),u&&mt(s),n)for(c=0;o=s[c++];)ht.test(o.type||"")&&n.push(o);return f}yt=s.createDocumentFragment().appendChild(s.createElement("div")),(_t=s.createElement("input")).setAttribute("type","radio"),_t.setAttribute("checked","checked"),_t.setAttribute("name","t"),yt.appendChild(_t),m.checkClone=yt.cloneNode(!0).cloneNode(!0).lastChild.checked,yt.innerHTML="",m.noCloneChecked=!!yt.cloneNode(!0).lastChild.defaultValue;var xt=s.documentElement,Ct=/^key/,$t=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Tt=/^([^.]*)(?:\.(.+)|)/;function kt(){return!0}function At(){return!1}function Et(){try{return s.activeElement}catch(t){}}function St(t,e,n,r,i,o){var s,a;if("object"==typeof e){for(a in"string"!=typeof n&&(r=r||n,n=void 0),e)St(t,a,n,r,e[a],o);return t}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=At;else if(!i)return t;return 1===o&&(s=i,(i=function(t){return C().off(t),s.apply(this,arguments)}).guid=s.guid||(s.guid=C.guid++)),t.each(function(){C.event.add(this,e,i,r,n)})}C.event={global:{},add:function(t,e,n,r,i){var o,s,a,l,u,c,f,d,p,h,v,g=Q.get(t);if(g)for(n.handler&&(n=(o=n).handler,i=o.selector),i&&C.find.matchesSelector(xt,i),n.guid||(n.guid=C.guid++),(l=g.events)||(l=g.events={}),(s=g.handle)||(s=g.handle=function(e){return void 0!==C&&C.event.triggered!==e.type?C.event.dispatch.apply(t,arguments):void 0}),u=(e=(e||"").match(F)||[""]).length;u--;)p=v=(a=Tt.exec(e[u])||[])[1],h=(a[2]||"").split(".").sort(),p&&(f=C.event.special[p]||{},p=(i?f.delegateType:f.bindType)||p,f=C.event.special[p]||{},c=C.extend({type:p,origType:v,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&C.expr.match.needsContext.test(i),namespace:h.join(".")},o),(d=l[p])||((d=l[p]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,s)||t.addEventListener&&t.addEventListener(p,s)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?d.splice(d.delegateCount++,0,c):d.push(c),C.event.global[p]=!0)},remove:function(t,e,n,r,i){var o,s,a,l,u,c,f,d,p,h,v,g=Q.hasData(t)&&Q.get(t);if(g&&(l=g.events)){for(u=(e=(e||"").match(F)||[""]).length;u--;)if(p=v=(a=Tt.exec(e[u])||[])[1],h=(a[2]||"").split(".").sort(),p){for(f=C.event.special[p]||{},d=l[p=(r?f.delegateType:f.bindType)||p]||[],a=a[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=o=d.length;o--;)c=d[o],!i&&v!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(d.splice(o,1),c.selector&&d.delegateCount--,f.remove&&f.remove.call(t,c));s&&!d.length&&(f.teardown&&!1!==f.teardown.call(t,h,g.handle)||C.removeEvent(t,p,g.handle),delete l[p])}else for(p in l)C.event.remove(t,p+e[u],n,r,!0);C.isEmptyObject(l)&&Q.remove(t,"handle events")}},dispatch:function(t){var e,n,r,i,o,s,a=C.event.fix(t),l=new Array(arguments.length),u=(Q.get(this,"events")||{})[a.type]||[],c=C.event.special[a.type]||{};for(l[0]=a,e=1;e=1))for(;u!==this;u=u.parentNode||this)if(1===u.nodeType&&("click"!==t.type||!0!==u.disabled)){for(o=[],s={},n=0;n-1:C.find(i,this,null,[u]).length),s[i]&&o.push(r);o.length&&a.push({elem:u,handlers:o})}return u=this,l\x20\t\r\n\f]*)[^>]*)\/>/gi,jt=/\s*$/g;function It(t,e){return O(t,"table")&&O(11!==e.nodeType?e:e.firstChild,"tr")&&C(t).children("tbody")[0]||t}function Rt(t){return t.type=(null!==t.getAttribute("type"))+"/"+t.type,t}function Lt(t){return"true/"===(t.type||"").slice(0,5)?t.type=t.type.slice(5):t.removeAttribute("type"),t}function Pt(t,e){var n,r,i,o,s,a,l,u;if(1===e.nodeType){if(Q.hasData(t)&&(o=Q.access(t),s=Q.set(e,o),u=o.events))for(i in delete s.handle,s.events={},u)for(n=0,r=u[i].length;n1&&"string"==typeof h&&!m.checkClone&&Dt.test(h))return t.each(function(i){var o=t.eq(i);v&&(e[0]=h.call(this,i,o.html())),Ft(o,e,n,r)});if(d&&(o=(i=wt(e,t[0].ownerDocument,!1,t,r)).firstChild,1===i.childNodes.length&&(i=o),o||r)){for(a=(s=C.map(gt(i,"script"),Rt)).length;f")},clone:function(t,e,n){var r,i,o,s,a,l,u,c=t.cloneNode(!0),f=C.contains(t.ownerDocument,t);if(!(m.noCloneChecked||1!==t.nodeType&&11!==t.nodeType||C.isXMLDoc(t)))for(s=gt(c),r=0,i=(o=gt(t)).length;r0&&mt(s,!f&>(t,"script")),c},cleanData:function(t){for(var e,n,r,i=C.event.special,o=0;void 0!==(n=t[o]);o++)if(G(n)){if(e=n[Q.expando]){if(e.events)for(r in e.events)i[r]?C.event.remove(n,r):C.removeEvent(n,r,e.handle);n[Q.expando]=void 0}n[Y.expando]&&(n[Y.expando]=void 0)}}}),C.fn.extend({detach:function(t){return Mt(this,t,!0)},remove:function(t){return Mt(this,t)},text:function(t){return W(this,function(t){return void 0===t?C.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=t)})},null,t,arguments.length)},append:function(){return Ft(this,arguments,function(t){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||It(this,t).appendChild(t)})},prepend:function(){return Ft(this,arguments,function(t){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var e=It(this,t);e.insertBefore(t,e.firstChild)}})},before:function(){return Ft(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this)})},after:function(){return Ft(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this.nextSibling)})},empty:function(){for(var t,e=0;null!=(t=this[e]);e++)1===t.nodeType&&(C.cleanData(gt(t,!1)),t.textContent="");return this},clone:function(t,e){return t=null!=t&&t,e=null==e?t:e,this.map(function(){return C.clone(this,t,e)})},html:function(t){return W(this,function(t){var e=this[0]||{},n=0,r=this.length;if(void 0===t&&1===e.nodeType)return e.innerHTML;if("string"==typeof t&&!jt.test(t)&&!vt[(pt.exec(t)||["",""])[1].toLowerCase()]){t=C.htmlPrefilter(t);try{for(;n=0&&(l+=Math.max(0,Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-o-l-a-.5))),l}function te(t,e,n){var r=qt(t),i=Bt(t,e,r),o="border-box"===C.css(t,"boxSizing",!1,r),s=o;if(Ut.test(i)){if(!n)return i;i="auto"}return s=s&&(m.boxSizingReliable()||i===t.style[e]),("auto"===i||!parseFloat(i)&&"inline"===C.css(t,"display",!1,r))&&(i=t["offset"+e[0].toUpperCase()+e.slice(1)],s=!0),(i=parseFloat(i)||0)+Yt(t,e,n||(o?"border":"content"),s,r,i)+"px"}function ee(t,e,n,r,i){return new ee.prototype.init(t,e,n,r,i)}C.extend({cssHooks:{opacity:{get:function(t,e){if(e){var n=Bt(t,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(t,e,n,r){if(t&&3!==t.nodeType&&8!==t.nodeType&&t.style){var i,o,s,a=Z(e),l=Vt.test(e),u=t.style;if(l||(e=Jt(a)),s=C.cssHooks[e]||C.cssHooks[a],void 0===n)return s&&"get"in s&&void 0!==(i=s.get(t,!1,r))?i:u[e];"string"===(o=typeof n)&&(i=it.exec(n))&&i[1]&&(n=lt(t,e,i),o="number"),null!=n&&n==n&&("number"===o&&(n+=i&&i[3]||(C.cssNumber[a]?"":"px")),m.clearCloneStyle||""!==n||0!==e.indexOf("background")||(u[e]="inherit"),s&&"set"in s&&void 0===(n=s.set(t,n,r))||(l?u.setProperty(e,n):u[e]=n))}},css:function(t,e,n,r){var i,o,s,a=Z(e);return Vt.test(e)||(e=Jt(a)),(s=C.cssHooks[e]||C.cssHooks[a])&&"get"in s&&(i=s.get(t,!0,n)),void 0===i&&(i=Bt(t,e,r)),"normal"===i&&e in Kt&&(i=Kt[e]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),C.each(["height","width"],function(t,e){C.cssHooks[e]={get:function(t,n,r){if(n)return!Wt.test(C.css(t,"display"))||t.getClientRects().length&&t.getBoundingClientRect().width?te(t,e,r):at(t,Xt,function(){return te(t,e,r)})},set:function(t,n,r){var i,o=qt(t),s="border-box"===C.css(t,"boxSizing",!1,o),a=r&&Yt(t,e,r,s,o);return s&&m.scrollboxSize()===o.position&&(a-=Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-parseFloat(o[e])-Yt(t,e,"border",!1,o)-.5)),a&&(i=it.exec(n))&&"px"!==(i[3]||"px")&&(t.style[e]=n,n=C.css(t,e)),Qt(0,n,a)}}}),C.cssHooks.marginLeft=zt(m.reliableMarginLeft,function(t,e){if(e)return(parseFloat(Bt(t,"marginLeft"))||t.getBoundingClientRect().left-at(t,{marginLeft:0},function(){return t.getBoundingClientRect().left}))+"px"}),C.each({margin:"",padding:"",border:"Width"},function(t,e){C.cssHooks[t+e]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[t+ot[r]+e]=o[r]||o[r-2]||o[0];return i}},"margin"!==t&&(C.cssHooks[t+e].set=Qt)}),C.fn.extend({css:function(t,e){return W(this,function(t,e,n){var r,i,o={},s=0;if(Array.isArray(e)){for(r=qt(t),i=e.length;s1)}}),C.Tween=ee,ee.prototype={constructor:ee,init:function(t,e,n,r,i,o){this.elem=t,this.prop=n,this.easing=i||C.easing._default,this.options=e,this.start=this.now=this.cur(),this.end=r,this.unit=o||(C.cssNumber[n]?"":"px")},cur:function(){var t=ee.propHooks[this.prop];return t&&t.get?t.get(this):ee.propHooks._default.get(this)},run:function(t){var e,n=ee.propHooks[this.prop];return this.options.duration?this.pos=e=C.easing[this.easing](t,this.options.duration*t,0,1,this.options.duration):this.pos=e=t,this.now=(this.end-this.start)*e+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):ee.propHooks._default.set(this),this}},ee.prototype.init.prototype=ee.prototype,ee.propHooks={_default:{get:function(t){var e;return 1!==t.elem.nodeType||null!=t.elem[t.prop]&&null==t.elem.style[t.prop]?t.elem[t.prop]:(e=C.css(t.elem,t.prop,""))&&"auto"!==e?e:0},set:function(t){C.fx.step[t.prop]?C.fx.step[t.prop](t):1!==t.elem.nodeType||null==t.elem.style[C.cssProps[t.prop]]&&!C.cssHooks[t.prop]?t.elem[t.prop]=t.now:C.style(t.elem,t.prop,t.now+t.unit)}}},ee.propHooks.scrollTop=ee.propHooks.scrollLeft={set:function(t){t.elem.nodeType&&t.elem.parentNode&&(t.elem[t.prop]=t.now)}},C.easing={linear:function(t){return t},swing:function(t){return.5-Math.cos(t*Math.PI)/2},_default:"swing"},C.fx=ee.prototype.init,C.fx.step={};var ne,re,ie=/^(?:toggle|show|hide)$/,oe=/queueHooks$/;function se(){re&&(!1===s.hidden&&n.requestAnimationFrame?n.requestAnimationFrame(se):n.setTimeout(se,C.fx.interval),C.fx.tick())}function ae(){return n.setTimeout(function(){ne=void 0}),ne=Date.now()}function le(t,e){var n,r=0,i={height:t};for(e=e?1:0;r<4;r+=2-e)i["margin"+(n=ot[r])]=i["padding"+n]=t;return e&&(i.opacity=i.width=t),i}function ue(t,e,n){for(var r,i=(ce.tweeners[e]||[]).concat(ce.tweeners["*"]),o=0,s=i.length;o1)},removeAttr:function(t){return this.each(function(){C.removeAttr(this,t)})}}),C.extend({attr:function(t,e,n){var r,i,o=t.nodeType;if(3!==o&&8!==o&&2!==o)return void 0===t.getAttribute?C.prop(t,e,n):(1===o&&C.isXMLDoc(t)||(i=C.attrHooks[e.toLowerCase()]||(C.expr.match.bool.test(e)?fe:void 0)),void 0!==n?null===n?void C.removeAttr(t,e):i&&"set"in i&&void 0!==(r=i.set(t,n,e))?r:(t.setAttribute(e,n+""),n):i&&"get"in i&&null!==(r=i.get(t,e))?r:null==(r=C.find.attr(t,e))?void 0:r)},attrHooks:{type:{set:function(t,e){if(!m.radioValue&&"radio"===e&&O(t,"input")){var n=t.value;return t.setAttribute("type",e),n&&(t.value=n),e}}}},removeAttr:function(t,e){var n,r=0,i=e&&e.match(F);if(i&&1===t.nodeType)for(;n=i[r++];)t.removeAttribute(n)}}),fe={set:function(t,e,n){return!1===e?C.removeAttr(t,n):t.setAttribute(n,n),n}},C.each(C.expr.match.bool.source.match(/\w+/g),function(t,e){var n=de[e]||C.find.attr;de[e]=function(t,e,r){var i,o,s=e.toLowerCase();return r||(o=de[s],de[s]=i,i=null!=n(t,e,r)?s:null,de[s]=o),i}});var pe=/^(?:input|select|textarea|button)$/i,he=/^(?:a|area)$/i;function ve(t){return(t.match(F)||[]).join(" ")}function ge(t){return t.getAttribute&&t.getAttribute("class")||""}function me(t){return Array.isArray(t)?t:"string"==typeof t&&t.match(F)||[]}C.fn.extend({prop:function(t,e){return W(this,C.prop,t,e,arguments.length>1)},removeProp:function(t){return this.each(function(){delete this[C.propFix[t]||t]})}}),C.extend({prop:function(t,e,n){var r,i,o=t.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&C.isXMLDoc(t)||(e=C.propFix[e]||e,i=C.propHooks[e]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(t,n,e))?r:t[e]=n:i&&"get"in i&&null!==(r=i.get(t,e))?r:t[e]},propHooks:{tabIndex:{get:function(t){var e=C.find.attr(t,"tabindex");return e?parseInt(e,10):pe.test(t.nodeName)||he.test(t.nodeName)&&t.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),m.optSelected||(C.propHooks.selected={get:function(t){var e=t.parentNode;return e&&e.parentNode&&e.parentNode.selectedIndex,null},set:function(t){var e=t.parentNode;e&&(e.selectedIndex,e.parentNode&&e.parentNode.selectedIndex)}}),C.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){C.propFix[this.toLowerCase()]=this}),C.fn.extend({addClass:function(t){var e,n,r,i,o,s,a,l=0;if(y(t))return this.each(function(e){C(this).addClass(t.call(this,e,ge(this)))});if((e=me(t)).length)for(;n=this[l++];)if(i=ge(n),r=1===n.nodeType&&" "+ve(i)+" "){for(s=0;o=e[s++];)r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(a=ve(r))&&n.setAttribute("class",a)}return this},removeClass:function(t){var e,n,r,i,o,s,a,l=0;if(y(t))return this.each(function(e){C(this).removeClass(t.call(this,e,ge(this)))});if(!arguments.length)return this.attr("class","");if((e=me(t)).length)for(;n=this[l++];)if(i=ge(n),r=1===n.nodeType&&" "+ve(i)+" "){for(s=0;o=e[s++];)for(;r.indexOf(" "+o+" ")>-1;)r=r.replace(" "+o+" "," ");i!==(a=ve(r))&&n.setAttribute("class",a)}return this},toggleClass:function(t,e){var n=typeof t,r="string"===n||Array.isArray(t);return"boolean"==typeof e&&r?e?this.addClass(t):this.removeClass(t):y(t)?this.each(function(n){C(this).toggleClass(t.call(this,n,ge(this),e),e)}):this.each(function(){var e,i,o,s;if(r)for(i=0,o=C(this),s=me(t);e=s[i++];)o.hasClass(e)?o.removeClass(e):o.addClass(e);else void 0!==t&&"boolean"!==n||((e=ge(this))&&Q.set(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===t?"":Q.get(this,"__className__")||""))})},hasClass:function(t){var e,n,r=0;for(e=" "+t+" ";n=this[r++];)if(1===n.nodeType&&(" "+ve(ge(n))+" ").indexOf(e)>-1)return!0;return!1}});var ye=/\r/g;C.fn.extend({val:function(t){var e,n,r,i=this[0];return arguments.length?(r=y(t),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?t.call(this,n,C(this).val()):t)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=C.map(i,function(t){return null==t?"":t+""})),(e=C.valHooks[this.type]||C.valHooks[this.nodeName.toLowerCase()])&&"set"in e&&void 0!==e.set(this,i,"value")||(this.value=i))})):i?(e=C.valHooks[i.type]||C.valHooks[i.nodeName.toLowerCase()])&&"get"in e&&void 0!==(n=e.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(ye,""):null==n?"":n:void 0}}),C.extend({valHooks:{option:{get:function(t){var e=C.find.attr(t,"value");return null!=e?e:ve(C.text(t))}},select:{get:function(t){var e,n,r,i=t.options,o=t.selectedIndex,s="select-one"===t.type,a=s?null:[],l=s?o+1:i.length;for(r=o<0?l:s?o:0;r-1)&&(n=!0);return n||(t.selectedIndex=-1),o}}}}),C.each(["radio","checkbox"],function(){C.valHooks[this]={set:function(t,e){if(Array.isArray(e))return t.checked=C.inArray(C(t).val(),e)>-1}},m.checkOn||(C.valHooks[this].get=function(t){return null===t.getAttribute("value")?"on":t.value})}),m.focusin="onfocusin"in n;var _e=/^(?:focusinfocus|focusoutblur)$/,be=function(t){t.stopPropagation()};C.extend(C.event,{trigger:function(t,e,r,i){var o,a,l,u,c,f,d,p,v=[r||s],g=h.call(t,"type")?t.type:t,m=h.call(t,"namespace")?t.namespace.split("."):[];if(a=p=l=r=r||s,3!==r.nodeType&&8!==r.nodeType&&!_e.test(g+C.event.triggered)&&(g.indexOf(".")>-1&&(g=(m=g.split(".")).shift(),m.sort()),c=g.indexOf(":")<0&&"on"+g,(t=t[C.expando]?t:new C.Event(g,"object"==typeof t&&t)).isTrigger=i?2:3,t.namespace=m.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=r),e=null==e?[t]:C.makeArray(e,[t]),d=C.event.special[g]||{},i||!d.trigger||!1!==d.trigger.apply(r,e))){if(!i&&!d.noBubble&&!_(r)){for(u=d.delegateType||g,_e.test(u+g)||(a=a.parentNode);a;a=a.parentNode)v.push(a),l=a;l===(r.ownerDocument||s)&&v.push(l.defaultView||l.parentWindow||n)}for(o=0;(a=v[o++])&&!t.isPropagationStopped();)p=a,t.type=o>1?u:d.bindType||g,(f=(Q.get(a,"events")||{})[t.type]&&Q.get(a,"handle"))&&f.apply(a,e),(f=c&&a[c])&&f.apply&&G(a)&&(t.result=f.apply(a,e),!1===t.result&&t.preventDefault());return t.type=g,i||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),e)||!G(r)||c&&y(r[g])&&!_(r)&&((l=r[c])&&(r[c]=null),C.event.triggered=g,t.isPropagationStopped()&&p.addEventListener(g,be),r[g](),t.isPropagationStopped()&&p.removeEventListener(g,be),C.event.triggered=void 0,l&&(r[c]=l)),t.result}},simulate:function(t,e,n){var r=C.extend(new C.Event,n,{type:t,isSimulated:!0});C.event.trigger(r,null,e)}}),C.fn.extend({trigger:function(t,e){return this.each(function(){C.event.trigger(t,e,this)})},triggerHandler:function(t,e){var n=this[0];if(n)return C.event.trigger(t,e,n,!0)}}),m.focusin||C.each({focus:"focusin",blur:"focusout"},function(t,e){var n=function(t){C.event.simulate(e,t.target,C.event.fix(t))};C.event.special[e]={setup:function(){var r=this.ownerDocument||this,i=Q.access(r,e);i||r.addEventListener(t,n,!0),Q.access(r,e,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=Q.access(r,e)-1;i?Q.access(r,e,i):(r.removeEventListener(t,n,!0),Q.remove(r,e))}}});var we=n.location,xe=Date.now(),Ce=/\?/;C.parseXML=function(t){var e;if(!t||"string"!=typeof t)return null;try{e=(new n.DOMParser).parseFromString(t,"text/xml")}catch(t){e=void 0}return e&&!e.getElementsByTagName("parsererror").length||C.error("Invalid XML: "+t),e};var $e=/\[\]$/,Te=/\r?\n/g,ke=/^(?:submit|button|image|reset|file)$/i,Ae=/^(?:input|select|textarea|keygen)/i;function Ee(t,e,n,r){var i;if(Array.isArray(e))C.each(e,function(e,i){n||$e.test(t)?r(t,i):Ee(t+"["+("object"==typeof i&&null!=i?e:"")+"]",i,n,r)});else if(n||"object"!==x(e))r(t,e);else for(i in e)Ee(t+"["+i+"]",e[i],n,r)}C.param=function(t,e){var n,r=[],i=function(t,e){var n=y(e)?e():e;r[r.length]=encodeURIComponent(t)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(t)||t.jquery&&!C.isPlainObject(t))C.each(t,function(){i(this.name,this.value)});else for(n in t)Ee(n,t[n],e,i);return r.join("&")},C.fn.extend({serialize:function(){return C.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var t=C.prop(this,"elements");return t?C.makeArray(t):this}).filter(function(){var t=this.type;return this.name&&!C(this).is(":disabled")&&Ae.test(this.nodeName)&&!ke.test(t)&&(this.checked||!dt.test(t))}).map(function(t,e){var n=C(this).val();return null==n?null:Array.isArray(n)?C.map(n,function(t){return{name:e.name,value:t.replace(Te,"\r\n")}}):{name:e.name,value:n.replace(Te,"\r\n")}}).get()}});var Se=/%20/g,Oe=/#.*$/,je=/([?&])_=[^&]*/,De=/^(.*?):[ \t]*([^\r\n]*)$/gm,Ne=/^(?:GET|HEAD)$/,Ie=/^\/\//,Re={},Le={},Pe="*/".concat("*"),Fe=s.createElement("a");function Me(t){return function(e,n){"string"!=typeof e&&(n=e,e="*");var r,i=0,o=e.toLowerCase().match(F)||[];if(y(n))for(;r=o[i++];)"+"===r[0]?(r=r.slice(1)||"*",(t[r]=t[r]||[]).unshift(n)):(t[r]=t[r]||[]).push(n)}}function Ue(t,e,n,r){var i={},o=t===Le;function s(a){var l;return i[a]=!0,C.each(t[a]||[],function(t,a){var u=a(e,n,r);return"string"!=typeof u||o||i[u]?o?!(l=u):void 0:(e.dataTypes.unshift(u),s(u),!1)}),l}return s(e.dataTypes[0])||!i["*"]&&s("*")}function qe(t,e){var n,r,i=C.ajaxSettings.flatOptions||{};for(n in e)void 0!==e[n]&&((i[n]?t:r||(r={}))[n]=e[n]);return r&&C.extend(!0,t,r),t}Fe.href=we.href,C.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:we.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(we.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Pe,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":C.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(t,e){return e?qe(qe(t,C.ajaxSettings),e):qe(C.ajaxSettings,t)},ajaxPrefilter:Me(Re),ajaxTransport:Me(Le),ajax:function(t,e){"object"==typeof t&&(e=t,t=void 0),e=e||{};var r,i,o,a,l,u,c,f,d,p,h=C.ajaxSetup({},e),v=h.context||h,g=h.context&&(v.nodeType||v.jquery)?C(v):C.event,m=C.Deferred(),y=C.Callbacks("once memory"),_=h.statusCode||{},b={},w={},x="canceled",$={readyState:0,getResponseHeader:function(t){var e;if(c){if(!a)for(a={};e=De.exec(o);)a[e[1].toLowerCase()]=e[2];e=a[t.toLowerCase()]}return null==e?null:e},getAllResponseHeaders:function(){return c?o:null},setRequestHeader:function(t,e){return null==c&&(t=w[t.toLowerCase()]=w[t.toLowerCase()]||t,b[t]=e),this},overrideMimeType:function(t){return null==c&&(h.mimeType=t),this},statusCode:function(t){var e;if(t)if(c)$.always(t[$.status]);else for(e in t)_[e]=[_[e],t[e]];return this},abort:function(t){var e=t||x;return r&&r.abort(e),T(0,e),this}};if(m.promise($),h.url=((t||h.url||we.href)+"").replace(Ie,we.protocol+"//"),h.type=e.method||e.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(F)||[""],null==h.crossDomain){u=s.createElement("a");try{u.href=h.url,u.href=u.href,h.crossDomain=Fe.protocol+"//"+Fe.host!=u.protocol+"//"+u.host}catch(t){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=C.param(h.data,h.traditional)),Ue(Re,h,e,$),c)return $;for(d in(f=C.event&&h.global)&&0==C.active++&&C.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Ne.test(h.type),i=h.url.replace(Oe,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(Se,"+")):(p=h.url.slice(i.length),h.data&&(h.processData||"string"==typeof h.data)&&(i+=(Ce.test(i)?"&":"?")+h.data,delete h.data),!1===h.cache&&(i=i.replace(je,"$1"),p=(Ce.test(i)?"&":"?")+"_="+xe+++p),h.url=i+p),h.ifModified&&(C.lastModified[i]&&$.setRequestHeader("If-Modified-Since",C.lastModified[i]),C.etag[i]&&$.setRequestHeader("If-None-Match",C.etag[i])),(h.data&&h.hasContent&&!1!==h.contentType||e.contentType)&&$.setRequestHeader("Content-Type",h.contentType),$.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+Pe+"; q=0.01":""):h.accepts["*"]),h.headers)$.setRequestHeader(d,h.headers[d]);if(h.beforeSend&&(!1===h.beforeSend.call(v,$,h)||c))return $.abort();if(x="abort",y.add(h.complete),$.done(h.success),$.fail(h.error),r=Ue(Le,h,e,$)){if($.readyState=1,f&&g.trigger("ajaxSend",[$,h]),c)return $;h.async&&h.timeout>0&&(l=n.setTimeout(function(){$.abort("timeout")},h.timeout));try{c=!1,r.send(b,T)}catch(t){if(c)throw t;T(-1,t)}}else T(-1,"No Transport");function T(t,e,s,a){var u,d,p,b,w,x=e;c||(c=!0,l&&n.clearTimeout(l),r=void 0,o=a||"",$.readyState=t>0?4:0,u=t>=200&&t<300||304===t,s&&(b=function(t,e,n){for(var r,i,o,s,a=t.contents,l=t.dataTypes;"*"===l[0];)l.shift(),void 0===r&&(r=t.mimeType||e.getResponseHeader("Content-Type"));if(r)for(i in a)if(a[i]&&a[i].test(r)){l.unshift(i);break}if(l[0]in n)o=l[0];else{for(i in n){if(!l[0]||t.converters[i+" "+l[0]]){o=i;break}s||(s=i)}o=o||s}if(o)return o!==l[0]&&l.unshift(o),n[o]}(h,$,s)),b=function(t,e,n,r){var i,o,s,a,l,u={},c=t.dataTypes.slice();if(c[1])for(s in t.converters)u[s.toLowerCase()]=t.converters[s];for(o=c.shift();o;)if(t.responseFields[o]&&(n[t.responseFields[o]]=e),!l&&r&&t.dataFilter&&(e=t.dataFilter(e,t.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(!(s=u[l+" "+o]||u["* "+o]))for(i in u)if((a=i.split(" "))[1]===o&&(s=u[l+" "+a[0]]||u["* "+a[0]])){!0===s?s=u[i]:!0!==u[i]&&(o=a[0],c.unshift(a[1]));break}if(!0!==s)if(s&&t.throws)e=s(e);else try{e=s(e)}catch(t){return{state:"parsererror",error:s?t:"No conversion from "+l+" to "+o}}}return{state:"success",data:e}}(h,b,$,u),u?(h.ifModified&&((w=$.getResponseHeader("Last-Modified"))&&(C.lastModified[i]=w),(w=$.getResponseHeader("etag"))&&(C.etag[i]=w)),204===t||"HEAD"===h.type?x="nocontent":304===t?x="notmodified":(x=b.state,d=b.data,u=!(p=b.error))):(p=x,!t&&x||(x="error",t<0&&(t=0))),$.status=t,$.statusText=(e||x)+"",u?m.resolveWith(v,[d,x,$]):m.rejectWith(v,[$,x,p]),$.statusCode(_),_=void 0,f&&g.trigger(u?"ajaxSuccess":"ajaxError",[$,h,u?d:p]),y.fireWith(v,[$,x]),f&&(g.trigger("ajaxComplete",[$,h]),--C.active||C.event.trigger("ajaxStop")))}return $},getJSON:function(t,e,n){return C.get(t,e,n,"json")},getScript:function(t,e){return C.get(t,void 0,e,"script")}}),C.each(["get","post"],function(t,e){C[e]=function(t,n,r,i){return y(n)&&(i=i||r,r=n,n=void 0),C.ajax(C.extend({url:t,type:e,dataType:i,data:n,success:r},C.isPlainObject(t)&&t))}}),C._evalUrl=function(t){return C.ajax({url:t,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,throws:!0})},C.fn.extend({wrapAll:function(t){var e;return this[0]&&(y(t)&&(t=t.call(this[0])),e=C(t,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&e.insertBefore(this[0]),e.map(function(){for(var t=this;t.firstElementChild;)t=t.firstElementChild;return t}).append(this)),this},wrapInner:function(t){return y(t)?this.each(function(e){C(this).wrapInner(t.call(this,e))}):this.each(function(){var e=C(this),n=e.contents();n.length?n.wrapAll(t):e.append(t)})},wrap:function(t){var e=y(t);return this.each(function(n){C(this).wrapAll(e?t.call(this,n):t)})},unwrap:function(t){return this.parent(t).not("body").each(function(){C(this).replaceWith(this.childNodes)}),this}}),C.expr.pseudos.hidden=function(t){return!C.expr.pseudos.visible(t)},C.expr.pseudos.visible=function(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)},C.ajaxSettings.xhr=function(){try{return new n.XMLHttpRequest}catch(t){}};var He={0:200,1223:204},Be=C.ajaxSettings.xhr();m.cors=!!Be&&"withCredentials"in Be,m.ajax=Be=!!Be,C.ajaxTransport(function(t){var e,r;if(m.cors||Be&&!t.crossDomain)return{send:function(i,o){var s,a=t.xhr();if(a.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(s in t.xhrFields)a[s]=t.xhrFields[s];for(s in t.mimeType&&a.overrideMimeType&&a.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest"),i)a.setRequestHeader(s,i[s]);e=function(t){return function(){e&&(e=r=a.onload=a.onerror=a.onabort=a.ontimeout=a.onreadystatechange=null,"abort"===t?a.abort():"error"===t?"number"!=typeof a.status?o(0,"error"):o(a.status,a.statusText):o(He[a.status]||a.status,a.statusText,"text"!==(a.responseType||"text")||"string"!=typeof a.responseText?{binary:a.response}:{text:a.responseText},a.getAllResponseHeaders()))}},a.onload=e(),r=a.onerror=a.ontimeout=e("error"),void 0!==a.onabort?a.onabort=r:a.onreadystatechange=function(){4===a.readyState&&n.setTimeout(function(){e&&r()})},e=e("abort");try{a.send(t.hasContent&&t.data||null)}catch(t){if(e)throw t}},abort:function(){e&&e()}}}),C.ajaxPrefilter(function(t){t.crossDomain&&(t.contents.script=!1)}),C.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(t){return C.globalEval(t),t}}}),C.ajaxPrefilter("script",function(t){void 0===t.cache&&(t.cache=!1),t.crossDomain&&(t.type="GET")}),C.ajaxTransport("script",function(t){var e,n;if(t.crossDomain)return{send:function(r,i){e=C(" - -@stop + function buildLdapResultsTableBody(users) + { + let body = '' + for (var i in users) { + body += '' + users[i].employee_number + '' + users[i].username + '' + users[i].firstname + '' + users[i].lastname + '' + users[i].email + '' + } + body += "" + return body; + } + +@endpush \ No newline at end of file diff --git a/resources/views/users/ldap.blade.php b/resources/views/users/ldap.blade.php index d83797b1b5..aa200552be 100644 --- a/resources/views/users/ldap.blade.php +++ b/resources/views/users/ldap.blade.php @@ -27,13 +27,17 @@ LDAP User Sync
- + +
+ @include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'location_id']) -
-
+
+ +
+
@@ -43,6 +47,7 @@ LDAP User Sync

{{ trans('admin/users/general.ldap_config_text') }}

+

LDAP Settings Page

@@ -63,7 +68,7 @@ LDAP User Sync @foreach (Session::get('summary') as $entry) - + {{ $entry['username'] }} {{ $entry['employee_number'] }} {{ $entry['firstname'] }} diff --git a/routes/api.php b/routes/api.php index d23adb046c..f7c81696c7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -552,7 +552,7 @@ Route::group(['prefix' => 'v1','namespace' => 'Api'], function () { /*--- Settings API ---*/ Route::get('settings/ldaptest', [ 'as' => 'api.settings.ldaptest', - 'uses' => 'SettingsController@ldaptest' + 'uses' => 'SettingsController@ldapAdSettingsTest' ]); Route::get('settings/login-attempts', [