Index: branches/5.2.x/core/install/install_data.sql =================================================================== diff -u -N -r14964 -r14973 --- branches/5.2.x/core/install/install_data.sql (.../install_data.sql) (revision 14964) +++ branches/5.2.x/core/install/install_data.sql (.../install_data.sql) (revision 14973) @@ -175,6 +175,8 @@ INSERT INTO Events (EventId, Event, ReplacementTags, Enabled, FrontEndOnly, Module, Description, Type, AllowChangingSender, AllowChangingRecipient) VALUES(DEFAULT, 'USER.NEW.PASSWORD', NULL, 1, 0, 'Core', 'Sends new password to an existing user', 0, 1, 0); INSERT INTO Events (EventId, Event, ReplacementTags, Enabled, FrontEndOnly, Module, Description, Type, AllowChangingSender, AllowChangingRecipient) VALUES(DEFAULT, 'USER.ADD.BYADMIN', NULL, 1, 0, 'Core', 'Sends password to a new user', 0, 1, 0); INSERT INTO Events (EventId, Event, ReplacementTags, Enabled, FrontEndOnly, Module, Description, Type, AllowChangingSender, AllowChangingRecipient) VALUES(DEFAULT, 'ROOT.RESET.PASSWORD', NULL, 1, 0, 'Core', 'Root Reset Password', 1, 1, 0); +INSERT INTO Events (EventId, Event, ReplacementTags, Enabled, FrontEndOnly, Module, Description, Type, AllowChangingSender, AllowChangingRecipient) VALUES(DEFAULT, 'USER.EMAIL.CHANGE.VERIFY', NULL, 1, 0, 'Core', 'Changed E-mail Verification', 0, 1, 1); +INSERT INTO Events (EventId, Event, ReplacementTags, Enabled, FrontEndOnly, Module, Description, Type, AllowChangingSender, AllowChangingRecipient) VALUES(DEFAULT, 'USER.EMAIL.CHANGE.UNDO', NULL, 1, 0, 'Core', 'Changed E-mail Rollback', 0, 1, 1); INSERT INTO IdGenerator VALUES ('100'); Index: branches/5.2.x/core/units/users/users_event_handler.php =================================================================== diff -u -N -r14971 -r14973 --- branches/5.2.x/core/units/users/users_event_handler.php (.../users_event_handler.php) (revision 14971) +++ branches/5.2.x/core/units/users/users_event_handler.php (.../users_event_handler.php) (revision 14973) @@ -1,6 +1,6 @@ getObject(); /* @var $object kDBItem */ - if ($event->Special == 'forgot') { + if ( $event->Special == 'forgot' ) { $object->SetDBField('PwResetConfirm', ''); $object->SetDBField('PwRequestTime_date', NULL); $object->SetDBField('PwRequestTime_time', NULL); } - $changed_fields = array_keys( $object->GetChangedFields() ); + $changed_fields = array_keys($object->GetChangedFields()); if ( $changed_fields && !in_array('Modified', $changed_fields) ) { $object->SetDBField('Modified_date', adodb_mktime()); $object->SetDBField('Modified_time', adodb_mktime()); } + + if ( !$this->Application->isAdmin && in_array('Email', $changed_fields) && ($event->Special != 'email-restore') ) { + $object->SetDBField('EmailVerified', 0); + } } /** @@ -1286,10 +1290,14 @@ $object =& $event->getObject(); /* @var $object UsersItem */ - if (!$this->Application->isAdmin || $object->IsTempTable()) { - return ; + if ( !$this->Application->isAdmin && ($event->Special != 'email-restore') ) { + $this->sendEmailChangeEvent($event); } + if ( !$this->Application->isAdmin || $object->IsTempTable() ) { + return; + } + $this->sendStatusChangeEvent($object->GetID(), $object->GetOriginalField('Status'), $object->GetDBField('Status')); } @@ -1433,6 +1441,57 @@ } /** + * Sends restore/validation email event on user email change + * + * @param kEvent $event + * @return void + * @access protected + */ + protected function sendEmailChangeEvent(kEvent &$event) + { + $object =& $event->getObject(); + /* @var $object UsersItem */ + + $new_email = $object->GetDBField('Email'); + $prev_email = $object->GetOriginalField('Email'); + + if ( !$new_email || ($prev_email == $new_email) ) { + return; + } + + $prev_emails = $object->GetDBField('PrevEmails'); + $prev_emails = $prev_emails ? unserialize($prev_emails) : Array (); + + $fields_hash = Array ( + 'PrevEmails' => serialize($prev_emails), + 'EmailVerified' => 0, + ); + + $user_id = $object->GetID(); + + if ( $prev_email ) { + $hash = md5(TIMENOW + $user_id); + $prev_emails[$hash] = $prev_email; + $fields_hash['PrevEmails'] = serialize($prev_emails); + + $send_params = Array ( + 'hash' => $hash, + 'to_email' => $prev_email, + 'to_name' => trim($object->GetDBField('FirstName') . ' ' . $object->GetDBField('LastName')), + ); + + $this->Application->EmailEventUser('USER.EMAIL.CHANGE.UNDO', null, $send_params); + } + + if ( $new_email ) { + $this->Application->EmailEventUser('USER.EMAIL.CHANGE.VERIFY', $user_id); + } + + // direct DB update, since USER.EMAIL.CHANGE.VERIFY puts verification code in user record, that we don't want to loose + $this->Conn->doUpdate($fields_hash, $object->TableName, 'PortalUserId = ' . $user_id); + } + + /** * OnAfterConfigRead for users * * @param kEvent $event Index: branches/5.2.x/core/units/helpers/user_helper.php =================================================================== diff -u -N -r14968 -r14973 --- branches/5.2.x/core/units/helpers/user_helper.php (.../user_helper.php) (revision 14968) +++ branches/5.2.x/core/units/helpers/user_helper.php (.../user_helper.php) (revision 14973) @@ -1,6 +1,6 @@ 'config:Users_AllowReset', 'activation' => 'config:UserEmailActivationTimeout', + 'verify_email' => 'config:Users_AllowReset', 'custom' => '', ); @@ -550,4 +551,47 @@ return $user_info['PortalUserId']; } + + /** + * Restores user's email, returns error label, if error occurred + * + * @param string $hash + * @return string + * @access public + */ + public function restoreEmail($hash) + { + if ( !preg_match('/^[a-f0-9]{32}$/', $hash) ) { + return 'invalid_hash'; + } + + $sql = 'SELECT PortalUserId, PrevEmails + FROM ' . TABLE_PREFIX . 'PortalUser + WHERE PrevEmails LIKE ' . $this->Conn->qstr('%' . $hash . '%'); + $user_info = $this->Conn->GetRow($sql); + + if ( $user_info === false ) { + return 'invalid_hash'; + } + + $prev_emails = $user_info['PrevEmails']; + $prev_emails = $prev_emails ? unserialize($prev_emails) : Array (); + + if ( !isset($prev_emails[$hash]) ) { + return 'invalid_hash'; + } + + $email_to_restore = $prev_emails[$hash]; + unset($prev_emails[$hash]); + + $object =& $this->Application->recallObject('u.email-restore', null, Array ('skip_autoload' => true)); + /* @var $object UsersItem */ + + $object->Load($user_info['PortalUserId']); + $object->SetDBField('PrevEmails', serialize($prev_emails)); + $object->SetDBField('Email', $email_to_restore); + $object->SetDBField('EmailVerified', 1); + + return $object->Update() ? '' : 'restore_impossible'; + } } Index: branches/5.2.x/admin/system_presets/simple/users_u.php =================================================================== diff -u -N -r14726 -r14973 --- branches/5.2.x/admin/system_presets/simple/users_u.php (.../users_u.php) (revision 14726) +++ branches/5.2.x/admin/system_presets/simple/users_u.php (.../users_u.php) (revision 14973) @@ -70,7 +70,7 @@ // fields to hide $hidden_fields = Array ( /* 'PortalUserId', 'Username', 'Password', 'FirstName','LastName', 'Company', 'Email', 'CreatedOn', - 'Phone', 'Fax', 'Street', 'Street2', 'City', 'State' , 'Zip', 'Country', 'ResourceId', 'Status', + 'Phone', 'Fax', 'Street', 'Street2', 'City', 'State' , 'Zip', 'Country', 'ResourceId', 'Status', 'EmailVerified', 'Modified', 'dob', 'tz',*/ 'IPAddress', /*'IsBanned', 'PwResetConfirm', 'PwRequestTime',*/ 'IPRestrictions', ); @@ -82,7 +82,7 @@ // fields to make required $required_fields = Array ( /*'PortalUserId',*/ 'Username', /*'Password', 'FirstName', 'LastName', 'Company', */'Email', /*'CreatedOn', - 'Phone', 'Fax', 'Street', 'Street2', 'City', 'State' , 'Zip', 'Country', 'ResourceId', 'Status', + 'Phone', 'Fax', 'Street', 'Street2', 'City', 'State' , 'Zip', 'Country', 'ResourceId', 'Status', 'EmailVerified', 'Modified', 'dob', 'tz', 'IPAddress', 'IsBanned', 'PwResetConfirm', 'PwRequestTime',*/ ); @@ -115,5 +115,5 @@ // 'Admins' => Array ('PortalUserId', 'Username', 'FirstName', 'LastName', 'Email'), // users list; section: Users Management -> Users - 'RegularUsers' => Array (/*'PortalUserId', 'Username', 'FirstName', 'LastName', 'Email',*/ 'PrimaryGroup', 'CreatedOn', 'Modified', /* 'Status',*/ 'IPAddress'), + 'RegularUsers' => Array (/*'PortalUserId', 'Username', 'FirstName', 'LastName', 'Email',*/ 'PrimaryGroup', 'CreatedOn', 'Modified', /* 'Status',*/ 'IPAddress', 'EmailVerified'), ); Index: branches/5.2.x/core/install/install_schema.sql =================================================================== diff -u -N -r14968 -r14973 --- branches/5.2.x/core/install/install_schema.sql (.../install_schema.sql) (revision 14968) +++ branches/5.2.x/core/install/install_schema.sql (.../install_schema.sql) (revision 14973) @@ -250,6 +250,7 @@ LastName varchar(255) NOT NULL DEFAULT '', Company varchar(255) NOT NULL DEFAULT '', Email varchar(255) NOT NULL DEFAULT '', + PrevEmails text, CreatedOn int(11) DEFAULT NULL, Phone varchar(255) NOT NULL DEFAULT '', Fax varchar(255) NOT NULL DEFAULT '', @@ -261,6 +262,7 @@ Country varchar(20) NOT NULL DEFAULT '', ResourceId int(11) NOT NULL DEFAULT '0', `Status` tinyint(4) NOT NULL DEFAULT '1', + EmailVerified tinyint(4) NOT NULL, Modified int(11) DEFAULT NULL, dob int(11) DEFAULT NULL, tz int(11) DEFAULT NULL, Index: branches/5.2.x/core/units/users/users_config.php =================================================================== diff -u -N -r14968 -r14973 --- branches/5.2.x/core/units/users/users_config.php (.../users_config.php) (revision 14968) +++ branches/5.2.x/core/units/users/users_config.php (.../users_config.php) (revision 14973) @@ -1,6 +1,6 @@ 0 ), 'IPRestrictions' => Array ('default' => NULL), + 'EmailVerified' => Array ( + 'formatter' => 'kOptionsFormatter', 'options' => Array (1 => 'la_Yes', 0 => 'la_No'), 'use_phrases' => 1, + 'default' => 0 + ), + 'PrevEmails' => Array ('default' => NULL), ), 'VirtualFields' => Array ( @@ -551,6 +556,8 @@ 'PrimaryGroupId' => Array ('type' => 'int'), 'OldStyleLogin' => Array ('type' => 'int', 'not_null' => 1), 'IPRestrictions' => Array ('type' => 'string'), + 'EmailVerified' => Array ('type' => 'int', 'not_null' => 1), + 'PrevEmails' => Array ('type' => 'string'), ), 'VirtualFields' => Array ( @@ -644,6 +651,7 @@ 'CreatedOn' => Array ('filter_block' => 'grid_date_range_filter', 'width' => 100), 'Modified' => Array ('filter_block' => 'grid_date_range_filter', 'width' => 100), 'IPAddress' => Array ('filter_block' => 'grid_date_range_filter', 'width' => 100, 'hidden' => 1), + 'EmailVerified' => Array ('filter_block' => 'grid_options_filter', 'width' => 100, 'hidden' => 1), ), ), ), Index: branches/5.2.x/core/units/users/users_tag_processor.php =================================================================== diff -u -N -r14941 -r14973 --- branches/5.2.x/core/units/users/users_tag_processor.php (.../users_tag_processor.php) (revision 14941) +++ branches/5.2.x/core/units/users/users_tag_processor.php (.../users_tag_processor.php) (revision 14973) @@ -1,6 +1,6 @@ Application->recallObject('UserHelper'); + /* @var $user_helper UserHelper */ + + $hash = $this->Application->GetVar('hash'); + $error_code = $user_helper->restoreEmail($hash); + + if ( $error_code ) { + // used for error reporting only -> rewrite code + theme (by Alex) + $object =& $this->getObject(Array ('skip_autoload' => true)); // TODO: change theme too + /* @var $object UsersItem */ + + $object->SetError('PwResetConfirm', 'restore', $params[$error_code]); + + return false; + } + + return true; + } + + /** * Returns error message set by given code type * * @param string $error_code @@ -133,6 +161,11 @@ 'code_is_not_valid' => 'lu_error_ActivationCodeNotValid', 'code_expired' => 'lu_error_ActivationCodeExpired', ), + + 'verify_email' => Array ( + 'code_is_not_valid' => 'lu_error_VerificationCodeNotValid', + 'code_expired' => 'lu_error_VerificationCodeExpired', + ), ); if ($code_type == 'custom') { @@ -264,33 +297,84 @@ } /** + * Returns link to revert e-mail change in user record + * + * @param Array $params + * @return string + * @access protected + */ + protected function UndoEmailChangeLink($params) + { + $params['hash'] = $this->Application->Parser->GetParam('hash'); + + if ( !$this->SelectParam($params, 'template,t') ) { + $params['template'] = $this->Application->GetVar('undo_email_template'); + } + + return $this->Application->ProcessParsedTag('m', 'Link', $params); + } + + /** * Activates user using given code * * @param Array $params + * @return string + * @access protected */ - function ActivateUser($params) + protected function ActivateUser($params) { - $passed_key = trim($this->Application->GetVar('user_key')); + $this->_updateAndLogin(Array ('Status' => STATUS_ACTIVE, 'EmailVerified' => 1)); + return ''; + } + + /** + * Marks user e-mail as verified using given code + * + * @param Array $params + * @return string + * @access protected + */ + protected function MarkUserEmailAsVerified($params) + { + $this->_updateAndLogin(Array ('EmailVerified' => 1)); + + return ''; + } + + /** + * Activates user using given code + * + * @param Array $fields_hash + * @return void + * @access protected + */ + protected function _updateAndLogin($fields_hash) + { $user_helper =& $this->Application->recallObject('UserHelper'); - /* @var $user_helper UserHelper */ + /* @var $user_helper UserHelper */ - $user =& $user_helper->getUserObject(); - $user->Load($passed_key, 'PwResetConfirm'); + $user =& $this->Application->recallObject($this->Prefix . '.activate', null, Array ('skip_autoload' => true)); + /* @var $user UsersItem */ + + $user->Load(trim($this->Application->GetVar('user_key')), 'PwResetConfirm'); + if ( !$user->isLoaded() ) { return ; } - $user->SetDBField('Status', STATUS_ACTIVE); + $user->SetFieldsFromHash($fields_hash); $user->SetDBField('PwResetConfirm', ''); $user->SetDBField('PwRequestTime_date', NULL); $user->SetDBField('PwRequestTime_time', NULL); $user->Update(); - if ( $user_helper->checkLoginPermission() ) { - $user_helper->loginUserById( $user->GetID() ); + $login_user =& $user_helper->getUserObject(); + $login_user->Load( $user->GetID() ); + + if ( ($login_user->GetDBField('Status') == STATUS_ACTIVE) && $user_helper->checkLoginPermission() ) { + $user_helper->loginUserById( $login_user->GetID() ); } } - } \ No newline at end of file Index: branches/5.2.x/core/install/upgrades.sql =================================================================== diff -u -N -r14968 -r14973 --- branches/5.2.x/core/install/upgrades.sql (.../upgrades.sql) (revision 14968) +++ branches/5.2.x/core/install/upgrades.sql (.../upgrades.sql) (revision 14973) @@ -2509,4 +2509,13 @@ UPDATE PortalUser SET FrontLanguage = 1 -WHERE UserType = 0; \ No newline at end of file +WHERE UserType = 0; + +ALTER TABLE PortalUser + ADD PrevEmails TEXT NULL AFTER Email, + ADD EmailVerified TINYINT NOT NULL AFTER `Status`; + +UPDATE PortalUser SET EmailVerified = 1; + +INSERT INTO Events (EventId, Event, ReplacementTags, Enabled, FrontEndOnly, Module, Description, Type, AllowChangingSender, AllowChangingRecipient) VALUES(DEFAULT, 'USER.EMAIL.CHANGE.VERIFY', NULL, 1, 0, 'Core', 'Changed E-mail Verification', 0, 1, 1); +INSERT INTO Events (EventId, Event, ReplacementTags, Enabled, FrontEndOnly, Module, Description, Type, AllowChangingSender, AllowChangingRecipient) VALUES(DEFAULT, 'USER.EMAIL.CHANGE.UNDO', NULL, 1, 0, 'Core', 'Changed E-mail Rollback', 0, 1, 1); Index: branches/5.2.x/core/admin_templates/users/users_edit.tpl =================================================================== diff -u -N -r14968 -r14973 --- branches/5.2.x/core/admin_templates/users/users_edit.tpl (.../users_edit.tpl) (revision 14968) +++ branches/5.2.x/core/admin_templates/users/users_edit.tpl (.../users_edit.tpl) (revision 14973) @@ -83,8 +83,9 @@ - + + Index: branches/5.2.x/core/install/english.lang =================================================================== diff -u -N -r14969 -r14973 --- branches/5.2.x/core/install/english.lang (.../english.lang) (revision 14969) +++ branches/5.2.x/core/install/english.lang (.../english.lang) (revision 14973) @@ -383,6 +383,7 @@ RW1haWxzIGluIFF1ZXVl RW1haWxzIFNlbnQ= RW1haWxzIFRvdGFs + RW1haWwgVmVyaWZpZWQ= RW5hYmxl RW5hYmxlZA== RW5hYmxlIENhY2hpbmcgZm9yIHRoaXMgU2VjdGlvbg== @@ -1586,15 +1587,17 @@ U3ViamVjdDogVGhhbmsgWW91IGZvciBDb250YWN0aW5nIFVzIQoKPHA+VGhhbmsgeW91IGZvciBjb250YWN0aW5nIHVzLiBXZSdsbCBiZSBpbiB0b3VjaCB3aXRoIHlvdSBzaG9ydGx5ITwvcD4= U3ViamVjdDogTmV3IGZvcm0gc3VibWlzc2lvbgoKPHA+Rm9ybSBoYXMgYmVlbiBzdWJtaXR0ZWQuIFBsZWFzZSBwcm9jZWVkIHRvIHRoZSBBZG1pbiBDb25zb2xlIHRvIHJldmlldyB0aGUgc3VibWlzc2lvbiE8L3A+ U3ViamVjdDogUm9vdCBSZXNldCBQYXNzd29yZAoKWW91ciBuZXcgcGFzc3dvcmQgaXM6IDxpbnAyOm1fUGFyYW0gbmFtZT0icGFzc3dvcmQiLz4= - U3ViamVjdDogSW4tcG9ydGFsIHJlZ2lzdHJhdGlvbgoKRGVhciA8aW5wMjp1X0ZpZWxkIG5hbWU9IkZpcnN0TmFtZSIgLz4gPGlucDI6dV9GaWVsZCBuYW1lPSJMYXN0TmFtZSIgLz4sDQoNClRoYW5rIHlvdSBmb3IgcmVnaXN0ZXJpbmcgb24gPGlucDI6bV9CYXNlVXJsLz4uIFlvdXIgcmVnaXN0cmF0aW9uIGlzIG5vdyBhY3RpdmUu - U3ViamVjdDogTmV3IFVzZXIgUmVnaXN0cmF0aW9uICg8aW5wMjp1X0ZpZWxkIG5hbWU9IlVzZXJuYW1lIi8+KQoKQSBuZXcgdXNlciAiPGlucDI6dV9GaWVsZCBuYW1lPSJVc2VybmFtZSIvPiIgaGFzIGJlZW4gYWRkZWQu + U3ViamVjdDogSW4tcG9ydGFsIHJlZ2lzdHJhdGlvbgoKRGVhciA8aW5wMjp1LnJlZ2lzdGVyX0ZpZWxkIG5hbWU9IkZpcnN0TmFtZSIgLz4gPGlucDI6dS5yZWdpc3Rlcl9GaWVsZCBuYW1lPSJMYXN0TmFtZSIgLz4sDQoNClRoYW5rIHlvdSBmb3IgcmVnaXN0ZXJpbmcgb24gPGlucDI6bV9CYXNlVXJsLz4uIFlvdXIgcmVnaXN0cmF0aW9uIGlzIG5vdyBhY3RpdmUuDQo8aW5wMjptX2lmIGNoZWNrPSJ1LnJlZ2lzdGVyX0ZpZWxkIiBuYW1lPSJFbWFpbCI+DQo8YnIvPjxici8+DQpQbGVhc2UgY2xpY2sgaGVyZSB0byB2ZXJpZnkgeW91ciBFLW1haWwgYWRkcmVzczoNCjxhIGhyZWY9IjxpbnAyOnUucmVnaXN0ZXJfQ29uZmlybVBhc3N3b3JkTGluayB0PSJwbGF0Zm9ybS9teV9hY2NvdW50L3ZlcmlmeV9lbWFpbCIgbm9fYW1wPSIxIi8+Ij48aW5wMjp1LnJlZ2lzdGVyX0NvbmZpcm1QYXNzd29yZExpbmsgdD0icGxhdGZvcm0vbXlfYWNjb3VudC92ZXJpZnlfZW1haWwiIG5vX2FtcD0iMSIvPjwvYT48YnIvPjxici8+DQo8L2lucDI6bV9pZj4= + U3ViamVjdDogTmV3IFVzZXIgUmVnaXN0cmF0aW9uICg8aW5wMjp1LnJlZ2lzdGVyX0ZpZWxkIG5hbWU9IlVzZXJuYW1lIi8+KQoKQSBuZXcgdXNlciAiPGlucDI6dS5yZWdpc3Rlcl9GaWVsZCBuYW1lPSdVc2VybmFtZScvPiIgaGFzIGJlZW4gYWRkZWQu U3ViamVjdDogTmV3IHVzZXIgaGFzIGJlZW4gY3JlYXRlZAoKRGVhciA8aW5wMjp1X0ZpZWxkIG5hbWU9IkZpcnN0TmFtZSIvPiwNCg0KQSBuZXcgdXNlciBoYXMgYmVlbiBjcmVhdGVkIGFuZCBhc3NpZ25lZCB0byB5b3UNCg0KTm93IHlvdSBjYW4gbG9naW4gdXNpbmcgdGhlIGZvbGxvd2luZyBjcmVkZW50aWFsczoNCg0KPGlucDI6bV9pZiBjaGVjaz0idV9GaWVsZCIgbmFtZT0iVXNlcm5hbWUiPlVzZXJuYW1lOiA8aW5wMjp1X0ZpZWxkIG5hbWU9IlVzZXJuYW1lIi8+PGlucDI6bV9lbHNlLz5FLW1haWw6IDxpbnAyOnVfRmllbGQgbmFtZT0iRW1haWwiLz48L2lucDI6bV9pZj4gDQpQYXNzd29yZDogPGlucDI6dV9GaWVsZCBuYW1lPSJQYXNzd29yZF9wbGFpbiIvPiANCg== - U3ViamVjdDogTmV3IFVzZXIgUmVnaXN0cmF0aW9uICg8aW5wMjp1X0ZpZWxkIG5hbWU9IlVzZXJuYW1lIi8+PGlucDI6bV9pZiBjaGVjaz0ibV9HZXRDb25maWciIG5hbWU9IlVzZXJfQWxsb3dfTmV3IiBlcXVhbHNfdG89IjQiPiAtIEFjdGl2YXRpb24gRW1haWw8L2lucDI6bV9pZj4pCgpEZWFyIDxpbnAyOnVfRmllbGQgbmFtZT0iRmlyc3ROYW1lIiAvPiA8aW5wMjp1X0ZpZWxkIG5hbWU9Ikxhc3ROYW1lIiAvPiw8YnIgLz4NCjxiciAvPg0KPGlucDI6bV9pZiBjaGVjaz0ibV9HZXRDb25maWciIG5hbWU9IlVzZXJfQWxsb3dfTmV3IiBlcXVhbHNfdG89IjQiPiBUaGFuayB5b3UgZm9yIHJlZ2lzdGVyaW5nIG9uIDxpbnAyOm1fTGluayB0ZW1wbGF0ZT0iaW5kZXgiLz4gd2Vic2l0ZS4gVG8gYWN0aXZhdGUgeW91ciByZWdpc3RyYXRpb24gcGxlYXNlIGZvbGxvdyBsaW5rIGJlbG93LiA8aW5wMjp1X0FjdGl2YXRpb25MaW5rIHRlbXBsYXRlPSJwbGF0Zm9ybS9sb2dpbi9hY3RpdmF0ZV9jb25maXJtIi8+IDxpbnAyOm1fZWxzZS8+IFRoYW5rIHlvdSBmb3IgcmVnaXN0ZXJpbmcgb24gPGlucDI6bV9MaW5rIHRlbXBsYXRlPSJpbmRleCIvPiB3ZWJzaXRlLiBZb3VyIHJlZ2lzdHJhdGlvbiB3aWxsIGJlIGFjdGl2ZSBhZnRlciBhcHByb3ZhbC4gPC9pbnAyOm1faWY+ - U3ViamVjdDogTmV3IFVzZXIgUmVnaXN0ZXJlZAoKQSBuZXcgdXNlciAiPGlucDI6dV9GaWVsZCBuYW1lPSJVc2VybmFtZSIvPiIgaGFzIHJlZ2lzdGVyZWQgYW5kIGlzIHBlbmRpbmcgYWRtaW5pc3RyYXRpdmUgYXBwcm92YWwu + U3ViamVjdDogTmV3IFVzZXIgUmVnaXN0cmF0aW9uICg8aW5wMjp1LnJlZ2lzdGVyX0ZpZWxkIG5hbWU9IlVzZXJuYW1lIi8+PGlucDI6bV9pZiBjaGVjaz0ibV9HZXRDb25maWciIG5hbWU9IlVzZXJfQWxsb3dfTmV3IiBlcXVhbHNfdG89IjQiPiAtIEFjdGl2YXRpb24gRW1haWw8L2lucDI6bV9pZj4pCgpEZWFyIDxpbnAyOnUucmVnaXN0ZXJfRmllbGQgbmFtZT0iRmlyc3ROYW1lIiAvPiA8aW5wMjp1LnJlZ2lzdGVyX0ZpZWxkIG5hbWU9Ikxhc3ROYW1lIiAvPiw8YnIgLz4NCjxiciAvPg0KPGlucDI6bV9pZiBjaGVjaz0ibV9HZXRDb25maWciIG5hbWU9IlVzZXJfQWxsb3dfTmV3IiBlcXVhbHNfdG89IjQiPg0KCVRoYW5rIHlvdSBmb3IgcmVnaXN0ZXJpbmcgb24gPGlucDI6bV9MaW5rIHRlbXBsYXRlPSJpbmRleCIvPiB3ZWJzaXRlLiBUbyBhY3RpdmF0ZSB5b3VyIHJlZ2lzdHJhdGlvbiBwbGVhc2UgZm9sbG93IGxpbmsgYmVsb3cuIDxpbnAyOnUucmVnaXN0ZXJfQWN0aXZhdGlvbkxpbmsgdGVtcGxhdGU9InBsYXRmb3JtL2xvZ2luL2FjdGl2YXRlX2NvbmZpcm0iLz4NCjxpbnAyOm1fZWxzZS8+DQoJVGhhbmsgeW91IGZvciByZWdpc3RlcmluZyBvbiA8aW5wMjptX0xpbmsgdGVtcGxhdGU9ImluZGV4Ii8+IHdlYnNpdGUuIFlvdXIgcmVnaXN0cmF0aW9uIHdpbGwgYmUgYWN0aXZlIGFmdGVyIGFwcHJvdmFsLiANCgkNCgk8aW5wMjptX2lmIGNoZWNrPSJ1LnJlZ2lzdGVyX0ZpZWxkIiBuYW1lPSJFbWFpbCI+DQoJCTxici8+PGJyLz4NCgkJUGxlYXNlIGNsaWNrIGhlcmUgdG8gdmVyaWZ5IHlvdXIgRS1tYWlsIGFkZHJlc3M6DQoJCTxhIGhyZWY9IjxpbnAyOnUucmVnaXN0ZXJfQ29uZmlybVBhc3N3b3JkTGluayB0PSJwbGF0Zm9ybS9teV9hY2NvdW50L3ZlcmlmeV9lbWFpbCIgbm9fYW1wPSIxIi8+Ij48aW5wMjp1LnJlZ2lzdGVyX0NvbmZpcm1QYXNzd29yZExpbmsgdD0icGxhdGZvcm0vbXlfYWNjb3VudC92ZXJpZnlfZW1haWwiIG5vX2FtcD0iMSIvPjwvYT48YnIvPjxici8+DQoJPC9pbnAyOm1faWY+DQo8L2lucDI6bV9pZj4= + U3ViamVjdDogTmV3IFVzZXIgUmVnaXN0ZXJlZAoKQSBuZXcgdXNlciAiPGlucDI6dS5yZWdpc3Rlcl9GaWVsZCBuYW1lPSJVc2VybmFtZSIvPiIgaGFzIHJlZ2lzdGVyZWQgYW5kIGlzIHBlbmRpbmcgYWRtaW5pc3RyYXRpdmUgYXBwcm92YWwu U3ViamVjdDogWW91ciBBY2NvdW50IGlzIEFjdGl2ZQoKV2VsY29tZSB0byA8aW5wMjptX0Jhc2VVcmwvPiENCg0KWW91ciB1c2VyIHJlZ2lzdHJhdGlvbiBoYXMgYmVlbiBhcHByb3ZlZC4gWW91ciB1c2VyIG5hbWUgaXM6ICI8aW5wMjp1X0ZpZWxkIG5hbWU9IlVzZXJuYW1lIi8+Ii4= U3ViamVjdDogTmV3IFVzZXIgQWNjb3VudCAiPGlucDI6dV9GaWVsZCBuYW1lPSJVc2VybmFtZSIvPiIgd2FzIEFwcHJvdmVkCgpVc2VyICI8aW5wMjp1X0ZpZWxkIG5hbWU9IlVzZXJuYW1lIi8+IiBoYXMgYmVlbiBhcHByb3ZlZC4= U3ViamVjdDogWW91ciBSZWdpc3RyYXRpb24gaGFzIGJlZW4gRGVuaWVkCgpZb3VyIHJlZ2lzdHJhdGlvbiBvbiA8YSBocmVmPSI8aW5wMjptX0Jhc2VVcmwvPiI+PGlucDI6bV9CYXNlVXJsLz48L2E+IHdlYnNpdGUgaGFzIGJlZW4gZGVuaWVkLg== U3ViamVjdDogVXNlciBSZWdpc3RyYXRpb24gZm9yICAiPGlucDI6dV9GaWVsZCBuYW1lPSJVc2VybmFtZSIvPiIgaGFzIGJlZW4gRGVuaWVkCgpVc2VyICI8aW5wMjp1X0ZpZWxkIG5hbWU9IlVzZXJuYW1lIi8+IiBoYXMgYmVlbiBkZW5pZWQu + U3ViamVjdDogQ2hhbmdlZCBFLW1haWwgUm9sbGJhY2sKCkhlbGxvLDxici8+PGJyLz4NCg0KSXQgc2VlbXMgdGhhdCB5b3UgaGF2ZSBjaGFuZ2VkIGUtbWFpbCBpbiB5b3VyIEluLXBvcnRhbCBhY2NvdW50LiBZb3UgbWF5IHVuZG8gdGhpcyBjaGFuZ2UgYnkgY2xpY2tpbmcgb24gdGhlIGxpbmsgYmVsb3c6PGJyLz48YnIvPg0KDQo8YSBocmVmPSI8aW5wMjp1X1VuZG9FbWFpbENoYW5nZUxpbmsgdGVtcGxhdGU9InBsYXRmb3JtL215X2FjY291bnQvcmVzdG9yZV9lbWFpbCIvPiI+PGlucDI6dV9VbmRvRW1haWxDaGFuZ2VMaW5rIHRlbXBsYXRlPSJwbGF0Zm9ybS9teV9hY2NvdW50L3Jlc3RvcmVfZW1haWwiLz48L2E+PGJyLz48YnIvPg0KDQpJZiB5b3UgYmVsaWV2ZSB5b3UgaGF2ZSByZWNlaXZlZCB0aGlzIGVtYWlsIGluIGVycm9yLCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwuIFlvdXIgYWNjb3VudCB3aWxsIGJlIGxpbmtlZCB0byBhbm90aGVyIGUtbWFpbCB1bmxlc3MgeW91IGhhdmUgY2xpY2tlZCBvbiB0aGUgYWJvdmUgbGluay4= + U3ViamVjdDogQ2hhbmdlZCBFLW1haWwgVmVyaWZpY2F0aW9uCgpIZWxsbyw8YnIvPjxici8+DQoNCkl0IHNlZW1zIHRoYXQgeW91IGhhdmUgY2hhbmdlZCBlLW1haWwgaW4geW91ciBJbi1wb3J0YWwgYWNjb3VudC4gUGxlYXNlIHZlcmlmeSB0aGlzIG5ldyBlLW1haWwgYnkgY2xpY2tpbmcgb24gdGhlIGxpbmsgYmVsb3c6PGJyLz48YnIvPg0KDQo8YSBocmVmPSI8aW5wMjp1X0NvbmZpcm1QYXNzd29yZExpbmsgdD0icGxhdGZvcm0vbXlfYWNjb3VudC92ZXJpZnlfZW1haWwiIG5vX2FtcD0iMSIvPiI+PGlucDI6dV9Db25maXJtUGFzc3dvcmRMaW5rIHQ9InBsYXRmb3JtL215X2FjY291bnQvdmVyaWZ5X2VtYWlsIiBub19hbXA9IjEiLz48L2E+PGJyLz48YnIvPg0KDQpJZiB5b3UgYmVsaWV2ZSB5b3UgaGF2ZSByZWNlaXZlZCB0aGlzIGVtYWlsIGluIGVycm9yLCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwuIFlvdXIgZW1haWwgd2lsbCBub3QgZ2V0IHZlcmlmaWVkIHN0YXR1cyB1bmxlc3MgeW91IGhhdmUgY2xpY2tlZCBvbiB0aGUgYWJvdmUgbGluay4NCg== U3ViamVjdDogTWVtYmVyc2hpcCBFeHBpcmF0aW9uIE5vdGljZQoKWW91ciBtZW1iZXJzaGlwIG9uIDxpbnAyOm1fQmFzZVVybC8+IHdlYnNpdGUgd2lsbCBzb29uIGV4cGlyZS4= U3ViamVjdDogTWVtYmVyc2hpcCBFeHBpcmF0aW9uIE5vdGljZSBmb3IgIjxpbnAyOnVfRmllbGQgbmFtZT0iVXNlcm5hbWUiLz4iIFNlbnQKClVzZXIgPGlucDI6dV9GaWVsZCBuYW1lPSJVc2VybmFtZSIvPiBtZW1iZXJzaGlwIHdpbGwgZXhwaXJlIHNvb24u U3ViamVjdDogWW91ciBNZW1iZXJzaGlwIEV4cGlyZWQKCllvdXIgbWVtYmVyc2hpcCBvbiA8aW5wMjptX0Jhc2VVcmwvPiB3ZWJzaXRlIGhhcyBleHBpcmVkLg==