Sync.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. namespace OCA\User_LDAP\Jobs;
  7. use OC\ServerNotAvailableException;
  8. use OCA\User_LDAP\AccessFactory;
  9. use OCA\User_LDAP\Configuration;
  10. use OCA\User_LDAP\ConnectionFactory;
  11. use OCA\User_LDAP\Helper;
  12. use OCA\User_LDAP\LDAP;
  13. use OCA\User_LDAP\Mapping\UserMapping;
  14. use OCP\AppFramework\Utility\ITimeFactory;
  15. use OCP\BackgroundJob\TimedJob;
  16. use OCP\EventDispatcher\IEventDispatcher;
  17. use OCP\IAvatarManager;
  18. use OCP\IConfig;
  19. use OCP\IDBConnection;
  20. use OCP\IUserManager;
  21. use OCP\Notification\IManager;
  22. use Psr\Log\LoggerInterface;
  23. class Sync extends TimedJob {
  24. public const MAX_INTERVAL = 12 * 60 * 60; // 12h
  25. public const MIN_INTERVAL = 30 * 60; // 30min
  26. protected LDAP $ldap;
  27. public function __construct(
  28. ITimeFactory $timeFactory,
  29. private IEventDispatcher $dispatcher,
  30. private IConfig $config,
  31. private IDBConnection $dbc,
  32. private IAvatarManager $avatarManager,
  33. private IUserManager $ncUserManager,
  34. private LoggerInterface $logger,
  35. private IManager $notificationManager,
  36. private UserMapping $mapper,
  37. private Helper $ldapHelper,
  38. private ConnectionFactory $connectionFactory,
  39. private AccessFactory $accessFactory,
  40. ) {
  41. parent::__construct($timeFactory);
  42. $this->setInterval(
  43. (int)$this->config->getAppValue(
  44. 'user_ldap',
  45. 'background_sync_interval',
  46. (string)self::MIN_INTERVAL
  47. )
  48. );
  49. $this->ldap = new LDAP($this->config->getSystemValueString('ldap_log_file'));
  50. }
  51. /**
  52. * Updates the interval
  53. *
  54. * The idea is to adjust the interval depending on the amount of known users
  55. * and the attempt to update each user one day. At most it would run every
  56. * 30 minutes, and at least every 12 hours.
  57. */
  58. public function updateInterval() {
  59. $minPagingSize = $this->getMinPagingSize();
  60. $mappedUsers = $this->mapper->count();
  61. $runsPerDay = ($minPagingSize === 0 || $mappedUsers === 0) ? self::MAX_INTERVAL
  62. : $mappedUsers / $minPagingSize;
  63. $interval = floor(24 * 60 * 60 / $runsPerDay);
  64. $interval = min(max($interval, self::MIN_INTERVAL), self::MAX_INTERVAL);
  65. $this->config->setAppValue('user_ldap', 'background_sync_interval', (string)$interval);
  66. }
  67. /**
  68. * returns the smallest configured paging size
  69. */
  70. protected function getMinPagingSize(): int {
  71. $configKeys = $this->config->getAppKeys('user_ldap');
  72. $configKeys = array_filter($configKeys, function ($key) {
  73. return str_contains($key, 'ldap_paging_size');
  74. });
  75. $minPagingSize = null;
  76. foreach ($configKeys as $configKey) {
  77. $pagingSize = $this->config->getAppValue('user_ldap', $configKey, $minPagingSize);
  78. $minPagingSize = $minPagingSize === null ? $pagingSize : min($minPagingSize, $pagingSize);
  79. }
  80. return (int)$minPagingSize;
  81. }
  82. /**
  83. * @param array $argument
  84. */
  85. public function run($argument) {
  86. $isBackgroundJobModeAjax = $this->config
  87. ->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
  88. if ($isBackgroundJobModeAjax) {
  89. return;
  90. }
  91. $cycleData = $this->getCycle();
  92. if ($cycleData === null) {
  93. $cycleData = $this->determineNextCycle();
  94. if ($cycleData === null) {
  95. $this->updateInterval();
  96. return;
  97. }
  98. }
  99. if (!$this->qualifiesToRun($cycleData)) {
  100. $this->updateInterval();
  101. return;
  102. }
  103. try {
  104. $expectMoreResults = $this->runCycle($cycleData);
  105. if ($expectMoreResults) {
  106. $this->increaseOffset($cycleData);
  107. } else {
  108. $this->determineNextCycle($cycleData);
  109. }
  110. $this->updateInterval();
  111. } catch (ServerNotAvailableException $e) {
  112. $this->determineNextCycle($cycleData);
  113. }
  114. }
  115. /**
  116. * @param array{offset: int, prefix: string} $cycleData
  117. * @return bool whether more results are expected from the same configuration
  118. */
  119. public function runCycle(array $cycleData): bool {
  120. $connection = $this->connectionFactory->get($cycleData['prefix']);
  121. $access = $this->accessFactory->get($connection);
  122. $access->setUserMapper($this->mapper);
  123. $filter = $access->combineFilterWithAnd([
  124. $access->connection->ldapUserFilter,
  125. $access->connection->ldapUserDisplayName . '=*',
  126. $access->getFilterPartForUserSearch('')
  127. ]);
  128. $results = $access->fetchListOfUsers(
  129. $filter,
  130. $access->userManager->getAttributes(),
  131. (int)$connection->ldapPagingSize,
  132. $cycleData['offset'],
  133. true
  134. );
  135. if ((int)$connection->ldapPagingSize === 0) {
  136. return false;
  137. }
  138. return count($results) >= (int)$connection->ldapPagingSize;
  139. }
  140. /**
  141. * Returns the info about the current cycle that should be run, if any,
  142. * otherwise null
  143. */
  144. public function getCycle(): ?array {
  145. $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true);
  146. if (count($prefixes) === 0) {
  147. return null;
  148. }
  149. $cycleData = [
  150. 'prefix' => $this->config->getAppValue('user_ldap', 'background_sync_prefix', 'none'),
  151. 'offset' => (int)$this->config->getAppValue('user_ldap', 'background_sync_offset', '0'),
  152. ];
  153. if (
  154. $cycleData['prefix'] !== 'none'
  155. && in_array($cycleData['prefix'], $prefixes)
  156. ) {
  157. return $cycleData;
  158. }
  159. return null;
  160. }
  161. /**
  162. * Save the provided cycle information in the DB
  163. *
  164. * @param array{prefix: ?string, offset: int} $cycleData
  165. */
  166. public function setCycle(array $cycleData): void {
  167. $this->config->setAppValue('user_ldap', 'background_sync_prefix', $cycleData['prefix']);
  168. $this->config->setAppValue('user_ldap', 'background_sync_offset', (string)$cycleData['offset']);
  169. }
  170. /**
  171. * returns data about the next cycle that should run, if any, otherwise
  172. * null. It also always goes for the next LDAP configuration!
  173. *
  174. * @param ?array{prefix: string, offset: int} $cycleData the old cycle
  175. * @return ?array{prefix: string, offset: int}
  176. */
  177. public function determineNextCycle(?array $cycleData = null): ?array {
  178. $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true);
  179. if (count($prefixes) === 0) {
  180. return null;
  181. }
  182. // get the next prefix in line and remember it
  183. $oldPrefix = $cycleData === null ? null : $cycleData['prefix'];
  184. $prefix = $this->getNextPrefix($oldPrefix);
  185. if ($prefix === null) {
  186. return null;
  187. }
  188. $cycleData['prefix'] = $prefix;
  189. $cycleData['offset'] = 0;
  190. $this->setCycle(['prefix' => $prefix, 'offset' => 0]);
  191. return $cycleData;
  192. }
  193. /**
  194. * Checks whether the provided cycle should be run. Currently, only the
  195. * last configuration change goes into account (at least one hour).
  196. *
  197. * @param array{prefix: string} $cycleData
  198. */
  199. public function qualifiesToRun(array $cycleData): bool {
  200. $lastChange = (int)$this->config->getAppValue('user_ldap', $cycleData['prefix'] . '_lastChange', '0');
  201. if ((time() - $lastChange) > 60 * 30) {
  202. return true;
  203. }
  204. return false;
  205. }
  206. /**
  207. * Increases the offset of the current cycle for the next run
  208. *
  209. * @param array{prefix: string, offset: int} $cycleData
  210. */
  211. protected function increaseOffset(array $cycleData): void {
  212. $ldapConfig = new Configuration($cycleData['prefix']);
  213. $cycleData['offset'] += (int)$ldapConfig->ldapPagingSize;
  214. $this->setCycle($cycleData);
  215. }
  216. /**
  217. * Determines the next configuration prefix based on the last one (if any)
  218. */
  219. protected function getNextPrefix(?string $lastPrefix): ?string {
  220. $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true);
  221. $noOfPrefixes = count($prefixes);
  222. if ($noOfPrefixes === 0) {
  223. return null;
  224. }
  225. $i = $lastPrefix === null ? false : array_search($lastPrefix, $prefixes, true);
  226. if ($i === false) {
  227. $i = -1;
  228. } else {
  229. $i++;
  230. }
  231. if (!isset($prefixes[$i])) {
  232. $i = 0;
  233. }
  234. return $prefixes[$i];
  235. }
  236. /**
  237. * Only used in tests
  238. */
  239. public function overwritePropertiesForTest(LDAP $ldapWrapper): void {
  240. $this->ldap = $ldapWrapper;
  241. }
  242. }