kenken999/php
0
1<?php2require_once( getabspath("classes/editpage.php") );3 4class UserInfoPage extends EditPage {5 6 protected $cachedRecord = null;7 8 protected $newRecordData = array();9 protected $oldRecordData = array();10 11 protected $updatedSuccessfully = false;12 13 14 function __construct( &$params ) {15 parent::__construct( $params );16 }17 18 19 public static function processUserInfoPageSecurity( $table ) {20 if( !Security::providerUsersInDb( Security::currentProvider() ) ) {21 HeaderRedirect("menu");22 }23 24 Security::processLogoutRequest();25 $sessionLevel = Security::userSessionLevel();26 if( !Security::isGuest() && $sessionLevel === LOGGED_2FSETUP_PENDING ) {27 if( !Security::verifySafeCSRF() ) {28 return false;29 }30 return true;31 }32 33 Security::tryRelogin();34 if( isLogged() && !Security::isGuest() ) {35 return true;36 }37 if( Security::isGuest() ) {38 HeaderRedirect("menu");39 return false;40 }41 redirectToLogin();42 return false;43 }44 45 public function process() {46 global $globalEvents;47 48 if( $globalEvents->exists("BeforeProcessUserinfo") )49 $globalEvents->BeforeProcessUserinfo( $this );50 51 if( $this->action == "enable2f" ) {52 $this->enable2f( postvalue("method"), postvalue("value") );53 return;54 }55 if( $this->action == "disable2f" ) {56 $this->disable2f( postvalue( "method" ));57 return;58 }59 if( $this->action == "skip2f" ) {60 $this->skip2f();61 return;62 }63 if( $this->action == "prefer2f" ) {64 $this->prefer2f( postvalue( "method" ));65 return;66 }67 68 if( $this->action == "confirm2f" ) {69 $this->confirm2f( postvalue( "method" ), postvalue( "code" ) );70 return;71 }72 73 if( $this->action == "edited" && $this->mode === USERINFO_SIMPLE ) {74 $this->processDataInput();75 $this->reportFieldsSaveStatus();76 return;77 }78 79 if( $this->action == "generateSecret" ) {80 $this->regenerateTotp();81 return;82 }83 84 85 if( $this->pSet->hasCaptcha() ) {86 $this->displayCaptcha();87 }88 89 $this->prgReadMessage();90 91 $this->doCommonAssignments();92 $this->prepareBreadcrumbs();93 $this->prepareCollapseButton();94 95 $this->prepare2fControls();96 if( $this->mode === USERINFO_SIMPLE ) {97 $this->prepareReadonlyFields();98 $this->prepareEditControls();99 } else {100 $this->hideUserinfoItems();101 }102 103 $this->addButtonHandlers();104 105 $this->addCommonJs();106 107 $this->prepareTwoFactorData();108 109 $templateFile = $this->templatefile;110 if( $globalEvents->exists("BeforeShowUserinfo") )111 $globalEvents->BeforeShowUserinfo( $this->xt, $templateFile, $this->getCurrentrecordInternal(), $this );112 113 $this->display( $this->templatefile );114 }115 116 protected function prepareTwoFactorData() {117 $twofData = $this->loadTwofData();118 $this->pageData["twoFactorData"] = $twofData->serialize();119 $this->pageData["twoFactorSettings"] = Security::twoFactorSettings();120 121 $this->pageData["redirectUrl"] = $_SESSION["MyURL"]122 ? $_SESSION["MyURL"]123 : GetTableLink("menu");124 }125 126 127 protected function regenerateTotp() {128 129 $twofData = $this->loadTwofData();130 $twofSettings =& Security::twoFactorSettings();131 if( !$twofSettings["available"] || !$twofSettings["types"]["totp"] ) {132 $this->send2fError( "TOTP authentication is not available" );133 }134 if( !$twofData->enable || $twofData->method != "totp" ) {135 $this->send2fError( "TOTP authentication is not enabled" );136 }137 $twofData->secret = generateTotpSecret();138 $twofData->confirmNeeded = true;139 storageSet( "2factor_update", $twofData->serialize() );140 141 // return data to browser142 $this->send2fConfirmRequest( $twofData );143 }144 145 protected function buildNewRecordData() {146 147 // define temporary arrays. These are required for ASP conversion148 $evalues = array();149 $efilename_values = array();150 $blobfields = array();151 152 foreach( $this->pSet->getPageFields() as $f ) {153 // don't read username client value154 if ( $f == Security::usernameField() || $f == Security::extIdField())155 continue;156 157 $control = $this->getControl( $f, $this->id );158 $control->readWebValue( $evalues, $blobfields, NULL, NULL, $efilename_values );159 }160 161 foreach( $efilename_values as $ekey => $value ) {162 $evalues[ $ekey ] = $value;163 }164 165 $this->newRecordData = $evalues;166 }167 168 /**169 * Process user data input and save it to the database table170 */171 public function processDataInput() {172 global $globalEvents;173 174 // CSRF protection175 if( !isPostRequest() )176 return false;177 178 $this->buildNewRecordData();179 180 if( !$this->recheckUserPermissions() ) {181 // prevent the page from reading database values182 $this->oldRecordData = $this->newRecordData;183 $this->cachedRecord = $this->newRecordData;184 return false;185 }186 187 $this->oldRecordData = $this->getFieldControlsData();188 189 if( !$this->checkCaptcha() )190 return false;191 192 $dcUpdate = $this->getUpdateDataCommand();193 $prep = $this->dataSource->prepareSQL( $dcUpdate );194 $userInfoWhere = $prep["where"];195 196 if( $globalEvents->exists("BeforeEditUserinfo") ) {197 $usermessage = "";198 199 $ret = $globalEvents->BeforeEditUserinfo(200 $this->newRecordData,201 $userInfoWhere,202 $this->oldRecordData,203 $usermessage,204 $this205 );206 207 if( $usermessage != "" )208 $this->setMessage( $usermessage );209 210 if( !$ret )211 return false;212 }213 214 if( !$this->checkDeniedDuplicatedValues() )215 return false;216 217 $this->updatedSuccessfully = $this->dataSource->updateSingle( $dcUpdate, false );218 if( !$this->updatedSuccessfully ) {219 $this->setDatabaseError( $this->dataSource->lastError() );220 return false;221 }222 $this->auditLog( $dcUpdate->values );223 224 // clear cached record225 $this->cachedRecord = null;226 227 $this->ProcessFiles();228 229 $this->setSuccessfulEditMessage();230 231 Security::refreshUserdata();232 Security::refreshDisplayName();233 234 $this->callAfterSuccessfulSave();235 236 if( $globalEvents->exists("AfterEditUserinfo") ) {237 // Add missing values from oldRecordData to newRecordData238 foreach( $this->oldRecordData as $f => $v ) {239 if( !isset( $this->newRecordData[ $f ] ) )240 $this->newRecordData[ $f ] = $v;241 }242 243 $globalEvents->AfterEditUserinfo(244 $this->newRecordData,245 $userInfoWhere,246 $this->oldRecordData,247 $this248 );249 }250 251 return true;252 }253 254 /**255 * @param String message256 */257 public function setDatabaseError( $message )258 {259 if( $this->mode != EDIT_INLINE )260 $this->message = "<strong>"."Record was NOT edited"."</strong><br><br>".$message;261 else262 $this->message = "Record was NOT edited".". ".$message;263 264 $this->messageType = MESSAGE_ERROR;265 }266 267 /**268 * Set a successful update message269 */270 protected function setSuccessfulEditMessage()271 {272 if( $this->isMessageSet() )273 return;274 275 $this->messageType = MESSAGE_INFO;276 $this->setMessage( "<strong>"."User profile updated"."</strong>" );277 278 $_SESSION["message_userinfo"] = $this->message . "";279 $_SESSION["message_userinfo_type"] = $this->messageType;280 }281 282 /**283 * Check if updated data contains duplicated values284 * @return Boolean285 */286 protected function checkDeniedDuplicatedValues() {287 return $this->checkDeniedDuplicatedForUpdate( $this->oldRecordData, $this->newRecordData );288 }289 290 291 public function getUpdateDataCommand() {292 $dc = $this->getSubsetDataCommand();293 $dc->values = &$this->newRecordData;294 return $dc;295 }296 297 /**298 * Call each control's afterSuccessfulSave method299 */300 protected function callAfterSuccessfulSave() {301 foreach( $this->getPageFields() as $f ) {302 $this->getControl( $f, $this->id )->afterSuccessfulSave();303 }304 }305 306 /**307 * send updated data to client308 */309 protected function reportFieldsSaveStatus() {310 $returnJSON = array();311 $returnJSON['success'] = $this->updatedSuccessfully;312 $returnJSON['message'] = $this->message;313 314 if( !$this->isCaptchaOk )315 $returnJSON['wrongCaptchaFieldName'] = $this->getCaptchaFieldName();316 317 if( $this->updatedSuccessfully ) {318 $data = $this->getFieldControlsData();319 if( !$data )320 $data = $this->newRecordData;321 322 foreach( $this->pSet->getPageFields() as $f ) {323 if( !IsBinaryType( $this->pSet->getFieldType( $f ) ) )324 $controlValues[ $f ] = $data[ $f ];325 }326 327 $returnJSON['controlValues'] = $controlValues;328 }329 330 echo printJSON( $returnJSON );331 exit();332 }333 334 protected function prgReadMessage()335 {336 if( !$this->isSimpleMode() || !isset($_SESSION["message_userinfo"]) )337 return;338 339 $this->setMessage( $_SESSION["message_userinfo"] );340 $this->messageType = $_SESSION["message_userinfo_type"];341 342 unset($_SESSION["message_userinfo"]);343 }344 345 public function getCurrentRecordInternal() {346 if( !is_null( $this->cachedRecord ) )347 return $this->cachedRecord;348 349 $dc = $this->getSubsetDataCommand();350 351 $fetchedArray = $this->dataSource->getSingle( $dc )->fetchAssoc();352 $this->cachedRecord = $this->cipherer->DecryptFetchedArray( $fetchedArray );353 354 return $this->cachedRecord;355 }356 357 function getDataSourceFilterCriteria( $ignoreFilterField = "" ) {358 return Security::currentUserCondition();359 }360 361 protected function doCommonAssignments() {362 363 $this->headerCommonAssign();364 $this->setLangParams();365 366 // display message367 $this->xt->assign("message_block", true);368 if( $this->isMessageSet() )369 {370 $this->xt->assign("message", $this->message );371 $this->xt->assign("message_class", $this->messageType == MESSAGE_ERROR ? "alert alert-danger" : "alert alert-success" );372 }373 else374 {375 $this->hideElement("message");376 }377 378 // body["end"] - this assignment is very important379 $this->assignFieldBlocksAndLabels();380 $this->assignBody();381 }382 383 protected function getFieldControlValues()384 {385 return $this->getFieldControlsData();386 }387 388 public function getEditFormat( $fName, $pSet = null ) {389 if( $fName == Security::usernameField() || $fName == Security::extIdField() ) {390 return EDIT_FORMAT_READONLY;391 }392 return parent::getEditFormat( $fName, $pSet );393 }394 395 396 public function prepareEditControls() {397 // prepare values398 $data = $this->getFieldControlValues();399 400 foreach( $this->pSet->getPageFields() as $fName ) {401 $this->prepareEditControl( $fName, $data );402 }403 404 // prepare userpic control?405 }406 407 public function getFieldControlsData() {408 return $this->getCurrentRecordInternal();409 }410 411 protected function recheckUserPermissions() {412 return true;413 }414 415 function element2Item( $name ) {416 if( $name == "message" ) {417 return array( "fields_message" );418 }419 return parent::element2Item( $name );420 }421 422 protected function prepare2fControls() {423 if( !Security::twoFactorAvailable() )424 return;425 $data = $this->getCurrentRecordInternal();426 if( !$data ) {427 return;428 }429 $this->hideItemType( 'twofactor_setup_comment' );430 431 $twofSettings = &Security::twoFactorSettings();432 $this->xt->assign( "twof_phone_value", $data[ $twofSettings["phoneField"] ] );433 $this->xt->assign( "twof_email_value", $data[ $twofSettings["emailField"] ] );434 435 $userOption = $data[ $twofSettings["twoFactorField"] ] + 0;436 $settings =& Security::twoFactorSettings();437 $enabledMethods = Security::twoFactorEnabledMethods( $userOption );438 $preferred = Security::twoFactorPreferredMethod( $userOption );439 440 foreach( Security::twoFactorAllMethods() as $m ) {441 442 if( Security::twoFactorMethodEnabled( $userOption, $m ) ) {443 $this->xt->assign( "twof_enabled".$m, true );444 if( $settings["required"] && count($enabledMethods) == 1 ) {445 $this->xt->assign( "twof_required".$m, true );446 }447 }448 if( $m == $preferred ) {449 $this->xt->assign( "twof_preferred".$m, true );450 }451 452 }453 454 if( Security::userSessionLevel() == LOGGED_2FSETUP_PENDING ) {455 $this->xt->assign( "twofactor_continue", true );456 if( !$twofSettings["required"] ) {457 $this->xt->assign( "twofactor_skip", true );458 }459 if( count( $enabledMethods ) ) {460 $this->hideItemType("twofactor_skip");461 } else {462 $this->hideItemType("twofactor_continue");463 }464 } 465 466 }467 468 469 /**470 * Load current settings from the database471 * @return TwoFactorData472 */473 protected function loadTwofData() {474 475 $data = $this->getCurrentrecordInternal();476 if( !$data ) {477 return null;478 }479 $twofSettings =& Security::twoFactorSettings();480 481 $twofData = new TwoFactorData;482 483 $userOption = $data[ $twofSettings["twoFactorField"] ] + 0;484 485 $twofData->methods = Security::twoFactorEnabledMethods( $userOption );486 $twofData->preferred = Security::twoFactorPreferredMethod( $userOption );487 $twofData->secret = $data[ $twofSettings["codeField"] ];488 $twofData->email = $data[ $twofSettings["emailField"] ];489 $twofData->phone = $data[ $twofSettings["phoneField"] ];490 $twofData->required = $twofSettings["required"];491 return $twofData;492 493 }494 495 496 /**497 * Verify if user input conflicts with project 2FA settings498 */499 protected function verify2fSettings( $twofData ) {500 501 $twofSettings =& Security::twoFactorSettings();502 503 if( $twofData->enable && !$twofSettings["types"][ $twofData->method ]) {504 $this->send2fError( "Unsupported authentication type" );505 return false;506 }507 508 if( !$twofData->enable && $twofSettings["required"] ) {509 $twofData->enable = true;510 }511 return true;512 }513 514 /**515 * read 2FA data from the request and save it to the session516 */517 protected function enable2f( $method, $data ) {518 519 // CSRF protection520 if( !isPostRequest() )521 return;522 523 if( !Security::twoFactorMethodAvailable($method) ) {524 echo "unknown two factor authentication method";525 exit();526 }527 if( $method != TWOFACTOR_APP && !$data ) {528 echo "no phone number or email provided";529 exit();530 }531 532 533 $twofData = $this->loadTwofData();534 535 if( $method == TWOFACTOR_APP ) {536 $twofData->secret = generateTotpSecret();537 } else if( $method == TWOFACTOR_EMAIL ) {538 $twofData->email = $data;539 } else if( $method == TWOFACTOR_PHONE ) {540 $twofData->phone = $data;541 }542 543 $twofData->methods[ $method ] = true;544 $twofData->code = generateUserCode( GetGlobalData("smsCodeLength", 6) );545 global $debug2Factor;546 if( $debug2Factor ) {547 $twofData->code = 333;548 }549 // save data to session, send code to the user550 storageSet( "2factor_update", $twofData->serialize() );551 552 // send verification code to the user553 $this->send2fCode( $method, $data, $twofData->code );554 555 // return data to browser556 $this->send2fConfirmRequest( $method, $twofData );557 }558 559 560 protected function skip2f() {561 562 // CSRF protection563 if( !isPostRequest() )564 return;565 566 $twofSettings =& Security::twoFactorSettings();567 if( !$twofSettings["required"] && Security::userSessionLevel() == LOGGED_2FSETUP_PENDING ) {568 $twofData = $this->loadTwofData();569 Security::elevateSession();570 Security::auditLoginSuccess();571 Security::callAfterLogin();572 $this->send2fSuccess( $twofData );573 } else {574 $this->send2fError("Two factor authentication is required");575 }576 }577 578 protected function disable2f( $method ) {579 580 // CSRF protection581 if( !isPostRequest() )582 return;583 584 $twofData = $this->loadTwofData();585 unset( $twofData->methods[ $method ] );586 $this->save2fData( $twofData );587 $this->send2fSuccess( $twofData );588 }589 590 protected function prefer2f( $method ) {591 // CSRF protection592 if( !isPostRequest() )593 return;594 $twofData = $this->loadTwofData();595 $twofData->preferred = Security::twoFactorPreferredMethod( 596 Security::getTwoFactorValue( $twofData->methods, $method )597 );598 $this->save2fData( $twofData );599 $this->send2fSuccess( $twofData );600 }601 602 603 604 protected function send2fError( $message, $status = 'error' ) {605 echo my_json_encode( array(606 "status" => $status,607 "message" => $message608 ));609 exit();610 }611 612 /**613 * send email or txt message with the code614 */615 protected function send2fCode( $method, $data, $code ) {616 617 if( $method != TWOFACTOR_EMAIL && $method != TWOFACTOR_PHONE )618 return;619 $ret = Security::sendTwoFactorCode(620 $method,621 $data,622 $code623 );624 if( !$ret["success"] ) {625 $this->send2fError( "Error sending message. " . $ret["message"] );626 }627 }628 629 /**630 * send response data to client, order it display confirm prompt631 */632 protected function send2fConfirmRequest( $method, $twofData ) {633 $response = array(634 "status" => "confirm",635 "method" => $method636 );637 if( $method == TWOFACTOR_APP ) {638 // otpauth://totp/Project1:username?secret=...&issuer=Project1639 $response["secret"] = $twofData->secret;640 $response["totpUrl"] = $this->getTotpUrl( $twofData->secret );641 } else if( $method == TWOFACTOR_EMAIL ) {642 $response["email"] = $twofData->email;643 } else if( $method == TWOFACTOR_PHONE ) {644 $response["phone"] = $twofData->phone;645 }646 echo my_json_encode( $response );647 exit();648 }649 650 /**651 * inform user on successful 2FA settings update652 */653 protected function send2fSuccess( $twofData ) {654 $twofData->preferred = Security::twoFactorPreferredMethod( 655 Security::getTwoFactorValue( $twofData->methods, $twofData->preferred )656 );657 $response = array(658 "status" => "saved",659 "twoFactorData" => $twofData->serialize()660 );661 echo my_json_encode( $response );662 exit();663 }664 665 protected function getTotpUrl( $secret ) {666 $twofSettings =& Security::twoFactorSettings();667 $encodedProjectName = rawurlencode( $twofSettings["projectName"] );668 $encodedUsername = rawurlencode( $this->getUserName() );669 $encodedSecret = rawurlencode( $secret );670 return "otpauth://totp/" . $encodedProjectName . ":".$encodedUsername."?secret=". $encodedSecret ."&issuer=" . $encodedProjectName;671 }672 673 /**674 * Check user-entered code against the one saved in session.675 * Save new values in the database676 */677 protected function confirm2f( $method, $code ) {678 $twofData = TwoFactorData::deserialize( storageGet("2factor_update") );679 if( !$twofData ) {680 $this->send2fError( "Session is lost. Refresh the page and try again." );681 }682 if( !$this->verify2fCode( $method, $twofData, $code ) ) {683 // just wrong code, ask to reype684 $this->send2fError( "Wrong code", 'wrong' );685 }686 687 // save data in the database688 $this->save2fData( $twofData );689 if( Security::userSessionLevel() != LOGGED_FULL ) {690 Security::elevateSession();691 Security::auditLoginSuccess();692 Security::callAfterLogin();693 }694 $this->send2fSuccess( $twofData );695 }696 697 protected function verify2fCode( $method, $twofData, $code ) {698 if( $method == TWOFACTOR_EMAIL || $method == TWOFACTOR_PHONE ) {699 return $twofData->code == $code;700 }701 if( $method == TWOFACTOR_APP ) {702 return $code == calculateTotpCode( $twofData->secret );703 }704 }705 706 protected function save2fData( $twofData ) {707 $twofSettings =& Security::twoFactorSettings();708 709 $dc = $this->getSubsetDataCommand();710 711 $dc->values[ $twofSettings["twoFactorField"] ] = Security::getTwoFactorValue( $twofData->methods, $twofData->preferred );712 713 if( $twofData->methods[ TWOFACTOR_EMAIL] ) {714 $dc->values[ $twofSettings["emailField"] ] = $twofData->email;715 }716 if( $twofData->methods[ TWOFACTOR_PHONE ] ) {717 $dc->values[ $twofSettings["phoneField"] ] = $twofData->phone;718 }719 if( $twofData->methods[ TWOFACTOR_APP] ) {720 $dc->values[ $twofSettings["codeField"] ] = $twofData->secret;721 }722 723 if( !$this->dataSource->updateSingle( $dc, false ) ) {724 $this->send2fError( $this->dataSource->lastError() );725 }726 $this->auditLog( $dc->values );727 Security::refreshUserdata();728 729 }730 731 function addCommonJs()732 {733 parent::addCommonJs();734 $this->AddJSFile("include/qrcode/qrcode2.js");735 $this->AddJSFile("include/qrcode/jquery.qrcode.js");736 }737 738 protected function getUserName() {739 $sessionLevel = Security::userSessionLevel();740 if( $sessionLevel === LOGGED_FULL ) {741 return Security::getUserName();742 }743 return Security::provisionalUsername();744 745 }746 747 public static function readPageModeFromRequest() {748 if( postvalue("mode") == '2factor' || Security::userSessionLevel() == LOGGED_2FSETUP_PENDING ) {749 return USERINFO_2FACTOR;750 }751 return USERINFO_SIMPLE;752 }753 754 /**755 * Hide all items not needed for 2factor authentication setup756 */757 protected function hideUserinfoItems() {758 $this->hideAllFormItems( 'grid' );759 $this->showItemType( 'twofactor_label' );760 if( Security::userSessionLevel() === LOGGED_2FSETUP_PENDING ) {761 $this->showItemType( 'twofactor_setup_comment' );762 } else {763 $this->showItemType( 'twofactor_comment' );764 }765 766 $this->showItemType( 'twofactor_settings' );767 768 }769 770 public function setTemplateFile() {771 return RunnerPage::setTemplateFile();772 }773 774 protected function prepareBreadcrumbs() {775 $this->xt->assign( "breadcrumb", true );776 $this->xt->assign( "crumb_home_link", runner_htmlspecialchars( GetLocalLink("menu") ) );777 778 $crumb = array();779 $crumb["crumb_title_span"] = true;780 $crumb["crumb_title"] = $this->getPageTitle( $this->pageType, $this->tName, $this->pSet );781 $breadcrumb = array( $crumb );782 $this->xt->assign_loopsection( "crumb", $breadcrumb );783 }784 785 public function auditLog( $values ) {786 $audit = GetAuditObject();787 if( !$audit ) {788 return;789 }790 $keys = array( Security::currentUserIdField() => Security::getUserId() );791 $values[ Security::currentUserIdField() ] = Security::getUserId();792 $audit->LogEdit( Security::loginTable(), $values, Security::currentUserData(), $keys );793 }794 795 public function getSingleRecordCommand() {796 return $this->getSubsetDataCommand();797 }798 799 function createProjectSettings() {800 $this->pSet = new ProjectSettings( $this->tName, $this->pageType, $this->pageName, $this->pageTable );801 }802}803 804class TwoFactorData {805 /**806 * code to be sent to the user807 * @var string808 */809 public $code;810 811 812 public $methods;813 public $preferred;814 815 public $secret;816 public $email;817 public $phone;818 public $required;819 820 821 public function serialize() {822 $ret = array();823 $ret["code"] = $this->code;824 $ret["methods"] = $this->methods;825 $ret["preferred"] = $this->preferred;826 $ret["secret"] = $this->secret;827 $ret["email"] = $this->email;828 $ret["phone"] = $this->phone;829 $ret["required"] = $this->required;830 return $ret;831 }832 833 public static function deserialize( $params ) {834 $ret = new TwoFactorData;835 RunnerApply($ret, $params);836 return $ret;837 }838 839}840 841?>