VerifyUserData.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2017 Bjoern Schiessle <bjoern@schiessle.org>
  5. *
  6. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  7. * @author Bjoern Schiessle <bjoern@schiessle.org>
  8. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  9. * @author Côme Chilliet <come.chilliet@nextcloud.com>
  10. * @author Joas Schilling <coding@schilljs.com>
  11. * @author Lukas Reschke <lukas@statuscode.ch>
  12. * @author Morris Jobke <hey@morrisjobke.de>
  13. * @author Patrik Kernstock <info@pkern.at>
  14. * @author Roeland Jago Douma <roeland@famdouma.nl>
  15. *
  16. * @license GNU AGPL version 3 or any later version
  17. *
  18. * This program is free software: you can redistribute it and/or modify
  19. * it under the terms of the GNU Affero General Public License as
  20. * published by the Free Software Foundation, either version 3 of the
  21. * License, or (at your option) any later version.
  22. *
  23. * This program is distributed in the hope that it will be useful,
  24. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  25. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  26. * GNU Affero General Public License for more details.
  27. *
  28. * You should have received a copy of the GNU Affero General Public License
  29. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  30. *
  31. */
  32. namespace OCA\Settings\BackgroundJobs;
  33. use OCP\Accounts\IAccountManager;
  34. use OCP\Accounts\PropertyDoesNotExistException;
  35. use OCP\AppFramework\Http;
  36. use OCP\AppFramework\Utility\ITimeFactory;
  37. use OCP\BackgroundJob\IJobList;
  38. use OCP\BackgroundJob\Job;
  39. use OCP\Http\Client\IClientService;
  40. use OCP\IConfig;
  41. use OCP\IUserManager;
  42. use Psr\Log\LoggerInterface;
  43. class VerifyUserData extends Job {
  44. /** @var bool */
  45. private bool $retainJob = true;
  46. /** @var int max number of attempts to send the request */
  47. private int $maxTry = 24;
  48. /** @var int how much time should be between two tries (1 hour) */
  49. private int $interval = 3600;
  50. private string $lookupServerUrl;
  51. public function __construct(
  52. private IAccountManager $accountManager,
  53. private IUserManager $userManager,
  54. private IClientService $httpClientService,
  55. private LoggerInterface $logger,
  56. ITimeFactory $timeFactory,
  57. private IConfig $config,
  58. ) {
  59. parent::__construct($timeFactory);
  60. $lookupServerUrl = $config->getSystemValue('lookup_server', 'https://lookup.nextcloud.com');
  61. $this->lookupServerUrl = rtrim($lookupServerUrl, '/');
  62. }
  63. public function start(IJobList $jobList): void {
  64. if ($this->shouldRun($this->argument)) {
  65. parent::start($jobList);
  66. $jobList->remove($this, $this->argument);
  67. if ($this->retainJob) {
  68. $this->reAddJob($jobList, $this->argument);
  69. } else {
  70. $this->resetVerificationState();
  71. }
  72. }
  73. }
  74. protected function run($argument) {
  75. $try = (int)$argument['try'] + 1;
  76. switch ($argument['type']) {
  77. case IAccountManager::PROPERTY_WEBSITE:
  78. $result = $this->verifyWebsite($argument);
  79. break;
  80. case IAccountManager::PROPERTY_TWITTER:
  81. case IAccountManager::PROPERTY_EMAIL:
  82. $result = $this->verifyViaLookupServer($argument, $argument['type']);
  83. break;
  84. default:
  85. // no valid type given, no need to retry
  86. $this->logger->error($argument['type'] . ' is no valid type for user account data.');
  87. $result = true;
  88. }
  89. if ($result === true || $try > $this->maxTry) {
  90. $this->retainJob = false;
  91. }
  92. }
  93. /**
  94. * verify web page
  95. *
  96. * @param array $argument
  97. * @return bool true if we could check the verification code, otherwise false
  98. */
  99. protected function verifyWebsite(array $argument) {
  100. $result = false;
  101. $url = rtrim($argument['data'], '/') . '/.well-known/' . 'CloudIdVerificationCode.txt';
  102. $client = $this->httpClientService->newClient();
  103. try {
  104. $response = $client->get($url);
  105. } catch (\Exception $e) {
  106. return false;
  107. }
  108. if ($response->getStatusCode() === Http::STATUS_OK) {
  109. $result = true;
  110. $publishedCode = $response->getBody();
  111. // remove new lines and spaces
  112. $publishedCodeSanitized = trim(preg_replace('/\s\s+/', ' ', $publishedCode));
  113. $user = $this->userManager->get($argument['uid']);
  114. // we don't check a valid user -> give up
  115. if ($user === null) {
  116. $this->logger->error($argument['uid'] . ' doesn\'t exist, can\'t verify user data.');
  117. return $result;
  118. }
  119. $userAccount = $this->accountManager->getAccount($user);
  120. $websiteProp = $userAccount->getProperty(IAccountManager::PROPERTY_WEBSITE);
  121. $websiteProp->setVerified($publishedCodeSanitized === $argument['verificationCode']
  122. ? IAccountManager::VERIFIED
  123. : IAccountManager::NOT_VERIFIED
  124. );
  125. $this->accountManager->updateAccount($userAccount);
  126. }
  127. return $result;
  128. }
  129. protected function verifyViaLookupServer(array $argument, string $dataType): bool {
  130. if (empty($this->lookupServerUrl) ||
  131. $this->config->getAppValue('files_sharing', 'lookupServerUploadEnabled', 'yes') !== 'yes' ||
  132. $this->config->getSystemValue('has_internet_connection', true) === false) {
  133. return true;
  134. }
  135. $user = $this->userManager->get($argument['uid']);
  136. // we don't check a valid user -> give up
  137. if ($user === null) {
  138. $this->logger->info($argument['uid'] . ' doesn\'t exist, can\'t verify user data.');
  139. return true;
  140. }
  141. $cloudId = $user->getCloudId();
  142. $lookupServerData = $this->queryLookupServer($cloudId);
  143. // for some reasons we couldn't read any data from the lookup server, try again later
  144. if (empty($lookupServerData) || empty($lookupServerData[$dataType])) {
  145. return false;
  146. }
  147. // lookup server has verification data for wrong user data (e.g. email address), try again later
  148. if ($lookupServerData[$dataType]['value'] !== $argument['data']) {
  149. return false;
  150. }
  151. // lookup server hasn't verified the email address so far, try again later
  152. if ($lookupServerData[$dataType]['verified'] === IAccountManager::NOT_VERIFIED) {
  153. return false;
  154. }
  155. try {
  156. $userAccount = $this->accountManager->getAccount($user);
  157. $property = $userAccount->getProperty($dataType);
  158. $property->setVerified(IAccountManager::VERIFIED);
  159. $this->accountManager->updateAccount($userAccount);
  160. } catch (PropertyDoesNotExistException $e) {
  161. return false;
  162. }
  163. return true;
  164. }
  165. /**
  166. * @param string $cloudId
  167. * @return array
  168. */
  169. protected function queryLookupServer($cloudId) {
  170. try {
  171. $client = $this->httpClientService->newClient();
  172. $response = $client->get(
  173. $this->lookupServerUrl . '/users?search=' . urlencode($cloudId) . '&exactCloudId=1',
  174. [
  175. 'timeout' => 10,
  176. 'connect_timeout' => 3,
  177. ]
  178. );
  179. $body = json_decode($response->getBody(), true);
  180. if (is_array($body) && isset($body['federationId']) && $body['federationId'] === $cloudId) {
  181. return $body;
  182. }
  183. } catch (\Exception $e) {
  184. // do nothing, we will just re-try later
  185. }
  186. return [];
  187. }
  188. /**
  189. * re-add background job with new arguments
  190. *
  191. * @param IJobList $jobList
  192. * @param array $argument
  193. */
  194. protected function reAddJob(IJobList $jobList, array $argument) {
  195. $jobList->add(VerifyUserData::class,
  196. [
  197. 'verificationCode' => $argument['verificationCode'],
  198. 'data' => $argument['data'],
  199. 'type' => $argument['type'],
  200. 'uid' => $argument['uid'],
  201. 'try' => (int)$argument['try'] + 1,
  202. 'lastRun' => time()
  203. ]
  204. );
  205. }
  206. /**
  207. * test if it is time for the next run
  208. *
  209. * @param array $argument
  210. * @return bool
  211. */
  212. protected function shouldRun(array $argument) {
  213. $lastRun = (int)$argument['lastRun'];
  214. return ((time() - $lastRun) > $this->interval);
  215. }
  216. /**
  217. * reset verification state after max tries are reached
  218. */
  219. protected function resetVerificationState(): void {
  220. $user = $this->userManager->get($this->argument['uid']);
  221. if ($user !== null) {
  222. $userAccount = $this->accountManager->getAccount($user);
  223. try {
  224. $property = $userAccount->getProperty($this->argument['type']);
  225. $property->setVerified(IAccountManager::NOT_VERIFIED);
  226. $this->accountManager->updateAccount($userAccount);
  227. } catch (PropertyDoesNotExistException $e) {
  228. return;
  229. }
  230. }
  231. }
  232. }