diff --git a/.travis.yml b/.travis.yml index 891ddae..d01cd21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ php: - 5.5 - 5.6 - 7.0 - - hhvm env: global: @@ -14,7 +13,7 @@ env: matrix: allow_failures: - - php: hhvm + - php: 7.0 include: - php: 5.5 env: NC_DATABASE=sqlite diff --git a/CHANGELOG.md b/CHANGELOG.md index a94b2c2..ef85ac8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +nextcloud-spreedme (0.3.11) +* Temporary Passwords now are a lot shorter and easier to communicate + nextcloud-spreedme (0.3.10) * Support file sharing through Nextcloud. This is not of any interest for you unless you are using an WebRTC MCU / SFU. diff --git a/appinfo/database.xml b/appinfo/database.xml new file mode 100644 index 0000000..70cdb58 --- /dev/null +++ b/appinfo/database.xml @@ -0,0 +1,52 @@ + + + *dbname* + true + false + utf8 + + *dbprefix*spreedme_tps + + + id + integer + true + true + true + + + tp + text + true + 64 + + + userid + text + true + 64 + + + expiration + timestamp + true + + + spreedme_tps_id_index + true + true + + id + + + + spreedme_tps_tp_index + true + + tp + + + +
+
diff --git a/appinfo/info.xml b/appinfo/info.xml index ab56dcf..d3e7b4a 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -10,7 +10,7 @@ https://github.com/strukturag/nextcloud-spreedme/blob/master/README.md https://github.com/strukturag/nextcloud-spreedme/blob/master/README.md - 0.3.10 + 0.3.11 SpreedME tools https://github.com/strukturag/nextcloud-spreedme/issues @@ -18,7 +18,7 @@ 174436 - + https://raw.githubusercontent.com/strukturag/nextcloud-spreedme/master/screenshots/appstore/conference.gif https://raw.githubusercontent.com/strukturag/nextcloud-spreedme/master/screenshots/appstore/presentation.png diff --git a/appinfo/routes.php b/appinfo/routes.php index 60b48a0..a61cf15 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -19,18 +19,18 @@ ['name' => 'page#debug', 'url' => '/admin/debug', 'verb' => 'GET'], ['name' => 'page#generate_temporary_password', 'url' => '/admin/tp', 'verb' => 'GET'], - // API + // General API ['name' => 'api#get_config', 'url' => '/api/v1/config', 'verb' => 'GET'], ['name' => 'api#get_user_config', 'url' => '/api/v1/user/config', 'verb' => 'GET'], ['name' => 'api#get_token', 'url' => '/api/v1/user/token', 'verb' => 'GET'], - ['name' => 'api#get_token_with_temporary_password', 'url' => '/api/v1/token/withtp', 'verb' => 'POST'], - ['name' => 'api#generate_temporary_password', 'url' => '/api/v1/admin/tp', 'verb' => 'POST'], ['name' => 'api#save_config', 'url' => '/api/v1/admin/config', 'verb' => 'PATCH'], ['name' => 'api#regenerate_shared_secret', 'url' => '/api/v1/admin/config/regenerate/sharedsecret', 'verb' => 'POST'], - ['name' => 'api#regenerate_temporary_password_signing_key', 'url' => '/api/v1/admin/config/regenerate/tp-key', 'verb' => 'POST'], ['name' => 'api#generate_spreed_webrtc_config', 'url' => '/api/v1/admin/config/generate/spreed-webrtc-config', 'verb' => 'POST'], ['name' => 'api#download_file', 'url' => '/api/v1/file/download', 'verb' => 'GET'], - // File Transfer + // Temporary password API + ['name' => 'temporarypassword#generate_temporary_password', 'url' => '/api/v1/admin/tp', 'verb' => 'POST'], + ['name' => 'temporarypassword#get_token_with_temporary_password', 'url' => '/api/v1/token/withtp', 'verb' => 'POST'], + // File transfer API ['name' => 'filesharing#uploadAndShare', 'url' => '/api/v1/filetransfers', 'verb' => 'POST'], ['name' => 'filesharing#listShares', 'url' => '/api/v1/filetransfers', 'verb' => 'GET'], ], diff --git a/config/config.php.in b/config/config.php.in index 357ea4d..a66859f 100644 --- a/config/config.php.in +++ b/config/config.php.in @@ -42,10 +42,6 @@ class Config { // You can generate such a temporary password at: /index.php/apps/spreedme/admin/tp (Nextcloud admin user account required) const OWNCLOUD_TEMPORARY_PASSWORD_LOGIN_ENABLED = false; - // If 'OWNCLOUD_TEMPORARY_PASSWORD_LOGIN_ENABLED' is set to true, you also have to provide a signing key here (64-character HEX string) - // Generate it using `xxd -ps -l 32 -c 32 /dev/random` (better) or `openssl rand -hex 32` - const OWNCLOUD_TEMPORARY_PASSWORD_SIGNING_KEY = 'f20e1b84781d80570fef6e2969f61ba91ccb56922398a45eXXXXXXXXXXXXXXXX'; - private function __construct() { } diff --git a/controller/apicontroller.php b/controller/apicontroller.php index 48323fc..226468c 100644 --- a/controller/apicontroller.php +++ b/controller/apicontroller.php @@ -87,51 +87,6 @@ public function getToken() { return new DataResponse($_response); } - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function generateTemporaryPassword($userid, $expiration) { - $_response = array('success' => false); - // TODO(leon): Move this to user.php - if ($this->user->isSpreedMeAdmin() && $userid !== null && $expiration !== null) { - try { - $_response['tp'] = base64_encode(Security::generateTemporaryPassword($userid, $expiration)); - $_response['success'] = true; - } catch (\Exception $e) { - $_response['error'] = $e->getCode(); - } - } - - return new DataResponse($_response); - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage - */ - public function getTokenWithTemporaryPassword($tp) { - $tmp = base64_decode($tp, true); - // We support both base64 encoded and unencoded TPs - if ($tmp !== false) { - $tp = $tmp; - } - - $_response = array('success' => false); - if ($tp) { - try { - $token = Security::getSignedComboFromTemporaryPassword($tp); - $_response = array_merge($_response, $token); - $_response['success'] = true; - } catch (\Exception $e) { - $_response['error'] = $e->getCode(); - } - } - - return new DataResponse($_response); - } - public function saveConfig($config) { $allowedKeys = array( 'SPREED_WEBRTC_ORIGIN', @@ -155,12 +110,6 @@ public function saveConfig($config) { Helper::createServiceUserUnlessExists(); } break; - case 'OWNCLOUD_TEMPORARY_PASSWORD_LOGIN_ENABLED': - if ($value === 'true' && Helper::getDatabaseConfigValue('OWNCLOUD_TEMPORARY_PASSWORD_SIGNING_KEY') === '') { - // Also generate a 'Temporary Password signing key' - Security::regenerateTemporaryPasswordSigningKey(); - } - break; } } } @@ -186,19 +135,6 @@ public function regenerateSharedSecret() { return new DataResponse($_response); } - public function regenerateTemporaryPasswordSigningKey() { - // TODO(leon): Should we also allow Spreed.ME group admins to regenerate the signing key? - $_response = array('success' => false); - try { - Security::regenerateTemporaryPasswordSigningKey(); - $_response['success'] = true; - } catch (\Exception $e) { - $_response['error'] = $e->getCode(); - } - - return new DataResponse($_response); - } - public function generateSpreedWebRTCConfig() { $_response = array('success' => false); try { diff --git a/controller/temporarypasswordcontroller.php b/controller/temporarypasswordcontroller.php new file mode 100644 index 0000000..949b9dd --- /dev/null +++ b/controller/temporarypasswordcontroller.php @@ -0,0 +1,75 @@ + + * @copyright struktur AG 2016 + */ + +namespace OCA\SpreedME\Controller; + +use OCA\SpreedME\Security\TemporaryPasswordManager; +use OCA\SpreedME\User\User; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\DataResponse; +use OCP\IDBConnection; +use OCP\IRequest; + +class TemporaryPasswordController extends Controller { + + private $user; + private $temporaryPasswordManager; + + public function __construct($appName, IRequest $request, IDBConnection $db) { + parent::__construct($appName, $request); + + if (!empty($userId)) { + $this->user = new User($userId); + } else { + $this->user = new User(); + } + + $this->temporaryPasswordManager = new TemporaryPasswordManager($db); + } + + /** + * @NoAdminRequired + */ + public function generateTemporaryPassword($userid, $expiration) { + $_response = array('success' => false); + if ($this->user->isSpreedMeAdmin() && $userid !== null && $expiration !== null) { + try { + $_response['tp'] = $this->temporaryPasswordManager->generateTemporaryPassword($userid, $expiration); + $_response['success'] = true; + } catch (\Exception $e) { + $_response['error'] = $e->getCode(); + } + } + + return new DataResponse($_response); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + */ + public function getTokenWithTemporaryPassword($tp) { + $_response = array('success' => false); + if ($tp) { + try { + $token = $this->temporaryPasswordManager->getSignedComboFromTemporaryPassword($tp); + $_response = array_merge($_response, $token); + $_response['success'] = true; + } catch (\Exception $e) { + $_response['error'] = $e->getCode(); + } + } + + return new DataResponse($_response); + } + +} diff --git a/css/generateTP.css b/css/generateTP.css index 65fa6e4..d02b0a5 100644 --- a/css/generateTP.css +++ b/css/generateTP.css @@ -44,6 +44,24 @@ form button[type="submit"] { #ui-datepicker-div .ui-datepicker-header > a:nth-child(2) { right: 2px; } +#ui-datepicker-div .ui-datepicker-buttonpane { + margin: 0; +} +#ui-datepicker-div::after { + display: none; +} +#ui-datepicker-div button { + border: 1px solid black; +} +#ui-datepicker-div table { + margin: 0; +} +#ui-datepicker-div dl, #ui-datepicker-div dd { + padding: 0; +} +#ui-datepicker-div dl>dt { + text-align: left; +} #generated { margin-left: 3%; diff --git a/css/jquery-timepicker.css b/css/jquery-timepicker.css index 5b47d4b..61320a8 100644 --- a/css/jquery-timepicker.css +++ b/css/jquery-timepicker.css @@ -1,5 +1,5 @@ -/*! jQuery Timepicker Addon - v1.6.1 - 2015-11-14 +/*! jQuery Timepicker Addon - v1.6.3 - 2016-04-20 * http://trentrichardson.com/examples/timepicker -* Copyright (c) 2015 Trent Richardson; Licensed MIT */ +* Copyright (c) 2016 Trent Richardson; Licensed MIT */ .ui-timepicker-div .ui-widget-header{margin-bottom:8px}.ui-timepicker-div dl{text-align:left}.ui-timepicker-div dl dt{float:left;clear:left;padding:0 0 0 5px}.ui-timepicker-div dl dd{margin:0 10px 10px 40%}.ui-timepicker-div td{font-size:90%}.ui-tpicker-grid-label{background:0 0;border:0;margin:0;padding:0}.ui-timepicker-div .ui_tpicker_unit_hide{display:none}.ui-timepicker-div .ui_tpicker_time .ui_tpicker_time_input{background:0 0;color:inherit;border:0;outline:0;border-bottom:solid 1px #555;width:95%}.ui-timepicker-div .ui_tpicker_time .ui_tpicker_time_input:focus{border-bottom-color:#aaa}.ui-timepicker-rtl{direction:rtl}.ui-timepicker-rtl dl{text-align:right;padding:0 5px 0 0}.ui-timepicker-rtl dl dt{float:right;clear:right}.ui-timepicker-rtl dl dd{margin:0 40% 10px 10px}.ui-timepicker-div.ui-timepicker-oneLine{padding-right:2px}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time,.ui-timepicker-div.ui-timepicker-oneLine dt{display:none}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time_label{display:block;padding-top:2px}.ui-timepicker-div.ui-timepicker-oneLine dl{text-align:right}.ui-timepicker-div.ui-timepicker-oneLine dl dd,.ui-timepicker-div.ui-timepicker-oneLine dl dd>div{display:inline-block;margin:0}.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_minute:before,.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_second:before{content:':';display:inline-block}.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_millisec:before,.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_microsec:before{content:'.';display:inline-block}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide,.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide:before{display:none} diff --git a/debug/debug.php b/debug/debug.php index 1df2452..fddfcf2 100644 --- a/debug/debug.php +++ b/debug/debug.php @@ -57,16 +57,6 @@ private static function testOwncloudPhpConfigFile() { if (!ctype_xdigit(Helper::getConfigValue('SPREED_WEBRTC_SHAREDSECRET'))) { return 'Invalid SPREED_WEBRTC_SHAREDSECRET in config/config.php. Secret may only contain hexadecimal characters.'; } - - if (Helper::getConfigValue('OWNCLOUD_TEMPORARY_PASSWORD_LOGIN_ENABLED') === true) { - if (strlen(Helper::getConfigValue('OWNCLOUD_TEMPORARY_PASSWORD_SIGNING_KEY')) !== 64) { - return 'OWNCLOUD_TEMPORARY_PASSWORD_SIGNING_KEY in config/config.php must be a 64 character hexadecimal string.'; - } - - if (!ctype_xdigit(Helper::getConfigValue('OWNCLOUD_TEMPORARY_PASSWORD_SIGNING_KEY'))) { - return 'Invalid OWNCLOUD_TEMPORARY_PASSWORD_SIGNING_KEY in config/config.php. Key may only contain hexadecimal characters.'; - } - } } private static function testOwncloudJavascriptConfigFile() { diff --git a/doc/API.txt b/doc/API.txt index 96c0b7e..ada25ff 100644 --- a/doc/API.txt +++ b/doc/API.txt @@ -88,7 +88,7 @@ Available endpoints with request methods and content-type: /api/v1/admin/tp The admin/tp endpoint returns a temporary password. - This endpoint requires admin authentication. + This endpoint requires admin authentication + a valid Nextcloud CSRF requesttoken. POST application/x-www-form-urlencoded userid: The user id to invite @@ -96,7 +96,7 @@ Available endpoints with request methods and content-type: Response 200: { "success": true, - "tp": "MTQ3MTk0ODUwMDpleHQvdGVzdC81N2JjMGFjMTgzNWQ2NS4xNzIwNTQ2NjoyOmlxYUphS2duYVRVdUhySHZjR1lkY0xmaVNxMk5ZMGR6OVZVSU5oOU1sMzg9" + "tp": "658395259239" } @@ -144,18 +144,6 @@ Available endpoints with request methods and content-type: } - /api/v1/admin/config/regenerate/tp-key - - The admin/config/regenerate/tp-key endpoint generates and stores a new OWNCLOUD_TEMPORARY_PASSWORD_SIGNING_KEY. - This endpoint requires admin authentication + a valid Nextcloud CSRF requesttoken. - - POST application/x-www-form-urlencoded - Response 200: - { - "success": true - } - - /api/v1/filetransfers The filetransfers endpoint allows admins list existing and create new shares. diff --git a/errors/errorcodes.php b/errors/errorcodes.php index e862675..27a65f0 100644 --- a/errors/errorcodes.php +++ b/errors/errorcodes.php @@ -17,6 +17,7 @@ class ErrorCodes { const TEMPORARY_PASSWORD_NOT_ENABLED = 50101; const TEMPORARY_PASSWORD_INVALID = 50102; const TEMPORARY_PASSWORD_INVALID_USERID = 50103; + const TEMPORARY_PASSWORD_USERID_TOO_LONG = 50104; const DB_CONFIG_ERROR_CONFIG_PHP_EXISTS = 50201; const REMOTE_CONFIG_EMPTY = 50301; const REMOTE_CONFIG_INVALID_JSON = 50302; diff --git a/helper/helper.php b/helper/helper.php index 1e0ba31..ab17d48 100644 --- a/helper/helper.php +++ b/helper/helper.php @@ -224,9 +224,9 @@ public static function generateSpreedWebRTCConfig() { } $replace = array( '/webrtc/' => self::getDatabaseConfigValueOrDefault('SPREED_WEBRTC_BASEPATH'), - 'the-default-secret-do-not-keep-me' => Security::getRandomHexString(256 / 4), // 256 bit - 'the-default-encryption-block-key' => Security::getRandomHexString(256 / 4), // 256 bit - 'i-did-not-change-the-public-token-boo' => Security::getRandomHexString(256 / 4), // 256 bit + 'the-default-secret-do-not-keep-me' => Security::getRandomString(256 / 4), // 256 bit + 'the-default-encryption-block-key' => Security::getRandomString(256 / 4), // 256 bit + 'i-did-not-change-the-public-token-boo' => Security::getRandomString(256 / 4), // 256 bit '/absolute/path/to/nextcloud/apps/spreedme/extra' => self::getOwnAppPath() . 'extra', 'some-secret-do-not-keep' => self::getDatabaseConfigValue('SPREED_WEBRTC_SHAREDSECRET'), ); diff --git a/js/jquery-timepicker.js b/js/jquery-timepicker.js index 9b9d97d..f3777f7 100644 --- a/js/jquery-timepicker.js +++ b/js/jquery-timepicker.js @@ -1,5 +1,5 @@ -/*! jQuery Timepicker Addon - v1.6.1 - 2015-11-14 +/*! jQuery Timepicker Addon - v1.6.3 - 2016-04-20 * http://trentrichardson.com/examples/timepicker -* Copyright (c) 2015 Trent Richardson; Licensed MIT */ -!function(a){"function"==typeof define&&define.amd?define(["jquery","jquery-ui"],a):a(jQuery)}(function($){if($.ui.timepicker=$.ui.timepicker||{},!$.ui.timepicker.version){$.extend($.ui,{timepicker:{version:"1.6.1"}});var Timepicker=function(){this.regional=[],this.regional[""]={currentText:"Now",closeText:"Done",amNames:["AM","A"],pmNames:["PM","P"],timeFormat:"HH:mm",timeSuffix:"",timeOnlyTitle:"Choose Time",timeText:"Time",hourText:"Hour",minuteText:"Minute",secondText:"Second",millisecText:"Millisecond",microsecText:"Microsecond",timezoneText:"Time Zone",isRTL:!1},this._defaults={showButtonPanel:!0,timeOnly:!1,timeOnlyShowDate:!1,showHour:null,showMinute:null,showSecond:null,showMillisec:null,showMicrosec:null,showTimezone:null,showTime:!0,stepHour:1,stepMinute:1,stepSecond:1,stepMillisec:1,stepMicrosec:1,hour:0,minute:0,second:0,millisec:0,microsec:0,timezone:null,hourMin:0,minuteMin:0,secondMin:0,millisecMin:0,microsecMin:0,hourMax:23,minuteMax:59,secondMax:59,millisecMax:999,microsecMax:999,minDateTime:null,maxDateTime:null,maxTime:null,minTime:null,onSelect:null,hourGrid:0,minuteGrid:0,secondGrid:0,millisecGrid:0,microsecGrid:0,alwaysSetTime:!0,separator:" ",altFieldTimeOnly:!0,altTimeFormat:null,altSeparator:null,altTimeSuffix:null,altRedirectFocus:!0,pickerTimeFormat:null,pickerTimeSuffix:null,showTimepicker:!0,timezoneList:null,addSliderAccess:!1,sliderAccessArgs:null,controlType:"slider",oneLine:!1,defaultValue:null,parse:"strict",afterInject:null},$.extend(this._defaults,this.regional[""])};$.extend(Timepicker.prototype,{$input:null,$altInput:null,$timeObj:null,inst:null,hour_slider:null,minute_slider:null,second_slider:null,millisec_slider:null,microsec_slider:null,timezone_select:null,maxTime:null,minTime:null,hour:0,minute:0,second:0,millisec:0,microsec:0,timezone:null,hourMinOriginal:null,minuteMinOriginal:null,secondMinOriginal:null,millisecMinOriginal:null,microsecMinOriginal:null,hourMaxOriginal:null,minuteMaxOriginal:null,secondMaxOriginal:null,millisecMaxOriginal:null,microsecMaxOriginal:null,ampm:"",formattedDate:"",formattedTime:"",formattedDateTime:"",timezoneList:null,units:["hour","minute","second","millisec","microsec"],support:{},control:null,setDefaults:function(a){return extendRemove(this._defaults,a||{}),this},_newInst:function($input,opts){var tp_inst=new Timepicker,inlineSettings={},fns={},overrides,i;for(var attrName in this._defaults)if(this._defaults.hasOwnProperty(attrName)){var attrValue=$input.attr("time:"+attrName);if(attrValue)try{inlineSettings[attrName]=eval(attrValue)}catch(err){inlineSettings[attrName]=attrValue}}overrides={beforeShow:function(a,b){return $.isFunction(tp_inst._defaults.evnts.beforeShow)?tp_inst._defaults.evnts.beforeShow.call($input[0],a,b,tp_inst):void 0},onChangeMonthYear:function(a,b,c){$.isFunction(tp_inst._defaults.evnts.onChangeMonthYear)&&tp_inst._defaults.evnts.onChangeMonthYear.call($input[0],a,b,c,tp_inst)},onClose:function(a,b){tp_inst.timeDefined===!0&&""!==$input.val()&&tp_inst._updateDateTime(b),$.isFunction(tp_inst._defaults.evnts.onClose)&&tp_inst._defaults.evnts.onClose.call($input[0],a,b,tp_inst)}};for(i in overrides)overrides.hasOwnProperty(i)&&(fns[i]=opts[i]||this._defaults[i]||null);tp_inst._defaults=$.extend({},this._defaults,inlineSettings,opts,overrides,{evnts:fns,timepicker:tp_inst}),tp_inst.amNames=$.map(tp_inst._defaults.amNames,function(a){return a.toUpperCase()}),tp_inst.pmNames=$.map(tp_inst._defaults.pmNames,function(a){return a.toUpperCase()}),tp_inst.support=detectSupport(tp_inst._defaults.timeFormat+(tp_inst._defaults.pickerTimeFormat?tp_inst._defaults.pickerTimeFormat:"")+(tp_inst._defaults.altTimeFormat?tp_inst._defaults.altTimeFormat:"")),"string"==typeof tp_inst._defaults.controlType?("slider"===tp_inst._defaults.controlType&&"undefined"==typeof $.ui.slider&&(tp_inst._defaults.controlType="select"),tp_inst.control=tp_inst._controls[tp_inst._defaults.controlType]):tp_inst.control=tp_inst._defaults.controlType;var timezoneList=[-720,-660,-600,-570,-540,-480,-420,-360,-300,-270,-240,-210,-180,-120,-60,0,60,120,180,210,240,270,300,330,345,360,390,420,480,525,540,570,600,630,660,690,720,765,780,840];null!==tp_inst._defaults.timezoneList&&(timezoneList=tp_inst._defaults.timezoneList);var tzl=timezoneList.length,tzi=0,tzv=null;if(tzl>0&&"object"!=typeof timezoneList[0])for(;tzl>tzi;tzi++)tzv=timezoneList[tzi],timezoneList[tzi]={value:tzv,label:$.timepicker.timezoneOffsetString(tzv,tp_inst.support.iso8601)};return tp_inst._defaults.timezoneList=timezoneList,tp_inst.timezone=null!==tp_inst._defaults.timezone?$.timepicker.timezoneOffsetNumber(tp_inst._defaults.timezone):-1*(new Date).getTimezoneOffset(),tp_inst.hour=tp_inst._defaults.hourtp_inst._defaults.hourMax?tp_inst._defaults.hourMax:tp_inst._defaults.hour,tp_inst.minute=tp_inst._defaults.minutetp_inst._defaults.minuteMax?tp_inst._defaults.minuteMax:tp_inst._defaults.minute,tp_inst.second=tp_inst._defaults.secondtp_inst._defaults.secondMax?tp_inst._defaults.secondMax:tp_inst._defaults.second,tp_inst.millisec=tp_inst._defaults.millisectp_inst._defaults.millisecMax?tp_inst._defaults.millisecMax:tp_inst._defaults.millisec,tp_inst.microsec=tp_inst._defaults.microsectp_inst._defaults.microsecMax?tp_inst._defaults.microsecMax:tp_inst._defaults.microsec,tp_inst.ampm="",tp_inst.$input=$input,tp_inst._defaults.altField&&(tp_inst.$altInput=$(tp_inst._defaults.altField),tp_inst._defaults.altRedirectFocus===!0&&tp_inst.$altInput.css({cursor:"pointer"}).focus(function(){$input.trigger("focus")})),(0===tp_inst._defaults.minDate||0===tp_inst._defaults.minDateTime)&&(tp_inst._defaults.minDate=new Date),(0===tp_inst._defaults.maxDate||0===tp_inst._defaults.maxDateTime)&&(tp_inst._defaults.maxDate=new Date),void 0!==tp_inst._defaults.minDate&&tp_inst._defaults.minDate instanceof Date&&(tp_inst._defaults.minDateTime=new Date(tp_inst._defaults.minDate.getTime())),void 0!==tp_inst._defaults.minDateTime&&tp_inst._defaults.minDateTime instanceof Date&&(tp_inst._defaults.minDate=new Date(tp_inst._defaults.minDateTime.getTime())),void 0!==tp_inst._defaults.maxDate&&tp_inst._defaults.maxDate instanceof Date&&(tp_inst._defaults.maxDateTime=new Date(tp_inst._defaults.maxDate.getTime())),void 0!==tp_inst._defaults.maxDateTime&&tp_inst._defaults.maxDateTime instanceof Date&&(tp_inst._defaults.maxDate=new Date(tp_inst._defaults.maxDateTime.getTime())),tp_inst.$input.bind("focus",function(){tp_inst._onFocus()}),tp_inst},_addTimePicker:function(a){var b=$.trim(this.$altInput&&this._defaults.altFieldTimeOnly?this.$input.val()+" "+this.$altInput.val():this.$input.val());this.timeDefined=this._parseTime(b),this._limitMinMaxDateTime(a,!1),this._injectTimePicker(),this._afterInject()},_parseTime:function(a,b){if(this.inst||(this.inst=$.datepicker._getInst(this.$input[0])),b||!this._defaults.timeOnly){var c=$.datepicker._get(this.inst,"dateFormat");try{var d=parseDateTimeInternal(c,this._defaults.timeFormat,a,$.datepicker._getFormatConfig(this.inst),this._defaults);if(!d.timeObj)return!1;$.extend(this,d.timeObj)}catch(e){return $.timepicker.log("Error parsing the date/time string: "+e+"\ndate/time string = "+a+"\ntimeFormat = "+this._defaults.timeFormat+"\ndateFormat = "+c),!1}return!0}var f=$.datepicker.parseTime(this._defaults.timeFormat,a,this._defaults);return f?($.extend(this,f),!0):!1},_afterInject:function(){var a=this.inst.settings;$.isFunction(a.afterInject)&&a.afterInject.call(this)},_injectTimePicker:function(){var a=this.inst.dpDiv,b=this.inst.settings,c=this,d="",e="",f=null,g={},h={},i=null,j=0,k=0;if(0===a.find("div.ui-timepicker-div").length&&b.showTimepicker){var l=" ui_tpicker_unit_hide",m='
'+b.timeText+'
";for(j=0,k=this.units.length;k>j;j++){if(d=this.units[j],e=d.substr(0,1).toUpperCase()+d.substr(1),f=null!==b["show"+e]?b["show"+e]:this.support[d],g[d]=parseInt(b[d+"Max"]-(b[d+"Max"]-b[d+"Min"])%b["step"+e],10),h[d]=0,m+='
'+b[d+"Text"]+'
',f&&b[d+"Grid"]>0){if(m+='
',"hour"===d)for(var n=b[d+"Min"];n<=g[d];n+=parseInt(b[d+"Grid"],10)){h[d]++;var o=$.datepicker.formatTime(this.support.ampm?"hht":"HH",{hour:n},b);m+='"}else for(var p=b[d+"Min"];p<=g[d];p+=parseInt(b[d+"Grid"],10))h[d]++,m+='";m+="
'+o+"'+(10>p?"0":"")+p+"
"}m+="
"}var q=null!==b.showTimezone?b.showTimezone:this.support.timezone;m+='
'+b.timezoneText+"
",m+='
',m+="
";var r=$(m);for(b.timeOnly===!0&&(r.prepend('
'+b.timeOnlyTitle+"
"),a.find(".ui-datepicker-header, .ui-datepicker-calendar").hide()),j=0,k=c.units.length;k>j;j++)d=c.units[j],e=d.substr(0,1).toUpperCase()+d.substr(1),f=null!==b["show"+e]?b["show"+e]:this.support[d],c[d+"_slider"]=c.control.create(c,r.find(".ui_tpicker_"+d+"_slider"),d,c[d],b[d+"Min"],g[d],b["step"+e]),f&&b[d+"Grid"]>0&&(i=100*h[d]*b[d+"Grid"]/(g[d]-b[d+"Min"]),r.find(".ui_tpicker_"+d+" table").css({width:i+"%",marginLeft:b.isRTL?"0":i/(-2*h[d])+"%",marginRight:b.isRTL?i/(-2*h[d])+"%":"0",borderCollapse:"collapse"}).find("td").click(function(a){var b=$(this),e=b.html(),f=parseInt(e.replace(/[^0-9]/g),10),g=e.replace(/[^apm]/gi),h=b.data("for");"hour"===h&&(-1!==g.indexOf("p")&&12>f?f+=12:-1!==g.indexOf("a")&&12===f&&(f=0)),c.control.value(c,c[h+"_slider"],d,f),c._onTimeChange(),c._onSelectHandler()}).css({cursor:"pointer",width:100/h[d]+"%",textAlign:"center",overflow:"hidden"}));if(this.timezone_select=r.find(".ui_tpicker_timezone").append("").find("select"),$.fn.append.apply(this.timezone_select,$.map(b.timezoneList,function(a,b){return $("