1
0

Sync.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  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\IAvatarManager;
  17. use OCP\IConfig;
  18. use OCP\IDBConnection;
  19. use OCP\IUserManager;
  20. use OCP\Notification\IManager;
  21. use Psr\Log\LoggerInterface;
  22. class Sync extends TimedJob {
  23. public const MAX_INTERVAL = 12 * 60 * 60; // 12h
  24. public const MIN_INTERVAL = 30 * 60; // 30min
  25. /** @var Helper */
  26. protected $ldapHelper;
  27. /** @var LDAP */
  28. protected $ldap;
  29. /** @var UserMapping */
  30. protected $mapper;
  31. /** @var IConfig */
  32. protected $config;
  33. /** @var IAvatarManager */
  34. protected $avatarManager;
  35. /** @var IDBConnection */
  36. protected $dbc;
  37. /** @var IUserManager */
  38. protected $ncUserManager;
  39. /** @var LoggerInterface */
  40. protected $logger;
  41. /** @var IManager */
  42. protected $notificationManager;
  43. /** @var ConnectionFactory */
  44. protected $connectionFactory;
  45. /** @var AccessFactory */
  46. protected $accessFactory;
  47. public function __construct(ITimeFactory $time) {
  48. parent::__construct($time);
  49. $this->setInterval(
  50. (int)\OC::$server->getConfig()->getAppValue(
  51. 'user_ldap',
  52. 'background_sync_interval',
  53. (string)self::MIN_INTERVAL
  54. )
  55. );
  56. }
  57. /**
  58. * updates the interval
  59. *
  60. * the idea is to adjust the interval depending on the amount of known users
  61. * and the attempt to update each user one day. At most it would run every
  62. * 30 minutes, and at least every 12 hours.
  63. */
  64. public function updateInterval() {
  65. $minPagingSize = $this->getMinPagingSize();
  66. $mappedUsers = $this->mapper->count();
  67. $runsPerDay = ($minPagingSize === 0 || $mappedUsers === 0) ? self::MAX_INTERVAL
  68. : $mappedUsers / $minPagingSize;
  69. $interval = floor(24 * 60 * 60 / $runsPerDay);
  70. $interval = min(max($interval, self::MIN_INTERVAL), self::MAX_INTERVAL);
  71. $this->config->setAppValue('user_ldap', 'background_sync_interval', (string)$interval);
  72. }
  73. /**
  74. * returns the smallest configured paging size
  75. * @return int
  76. */
  77. protected function getMinPagingSize() {
  78. $configKeys = $this->config->getAppKeys('user_ldap');
  79. $configKeys = array_filter($configKeys, function ($key) {
  80. return str_contains($key, 'ldap_paging_size');
  81. });
  82. $minPagingSize = null;
  83. foreach ($configKeys as $configKey) {
  84. $pagingSize = $this->config->getAppValue('user_ldap', $configKey, $minPagingSize);
  85. $minPagingSize = $minPagingSize === null ? $pagingSize : min($minPagingSize, $pagingSize);
  86. }
  87. return (int)$minPagingSize;
  88. }
  89. /**
  90. * @param array $argument
  91. */
  92. public function run($argument) {
  93. $this->setArgument($argument);
  94. $isBackgroundJobModeAjax = $this->config
  95. ->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
  96. if ($isBackgroundJobModeAjax) {
  97. return;
  98. }
  99. $cycleData = $this->getCycle();
  100. if ($cycleData === null) {
  101. $cycleData = $this->determineNextCycle();
  102. if ($cycleData === null) {
  103. $this->updateInterval();
  104. return;
  105. }
  106. }
  107. if (!$this->qualifiesToRun($cycleData)) {
  108. $this->updateInterval();
  109. return;
  110. }
  111. try {
  112. $expectMoreResults = $this->runCycle($cycleData);
  113. if ($expectMoreResults) {
  114. $this->increaseOffset($cycleData);
  115. } else {
  116. $this->determineNextCycle($cycleData);
  117. }
  118. $this->updateInterval();
  119. } catch (ServerNotAvailableException $e) {
  120. $this->determineNextCycle($cycleData);
  121. }
  122. }
  123. /**
  124. * @param array $cycleData
  125. * @return bool whether more results are expected from the same configuration
  126. */
  127. public function runCycle($cycleData) {
  128. $connection = $this->connectionFactory->get($cycleData['prefix']);
  129. $access = $this->accessFactory->get($connection);
  130. $access->setUserMapper($this->mapper);
  131. $filter = $access->combineFilterWithAnd([
  132. $access->connection->ldapUserFilter,
  133. $access->connection->ldapUserDisplayName . '=*',
  134. $access->getFilterPartForUserSearch('')
  135. ]);
  136. $results = $access->fetchListOfUsers(
  137. $filter,
  138. $access->userManager->getAttributes(),
  139. (int)$connection->ldapPagingSize,
  140. $cycleData['offset'],
  141. true
  142. );
  143. if ((int)$connection->ldapPagingSize === 0) {
  144. return false;
  145. }
  146. return count($results) >= (int)$connection->ldapPagingSize;
  147. }
  148. /**
  149. * returns the info about the current cycle that should be run, if any,
  150. * otherwise null
  151. *
  152. * @return array|null
  153. */
  154. public function getCycle() {
  155. $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true);
  156. if (count($prefixes) === 0) {
  157. return null;
  158. }
  159. $cycleData = [
  160. 'prefix' => $this->config->getAppValue('user_ldap', 'background_sync_prefix', null),
  161. 'offset' => (int)$this->config->getAppValue('user_ldap', 'background_sync_offset', '0'),
  162. ];
  163. if (
  164. $cycleData['prefix'] !== null
  165. && in_array($cycleData['prefix'], $prefixes)
  166. ) {
  167. return $cycleData;
  168. }
  169. return null;
  170. }
  171. /**
  172. * Save the provided cycle information in the DB
  173. *
  174. * @param array $cycleData
  175. */
  176. public function setCycle(array $cycleData) {
  177. $this->config->setAppValue('user_ldap', 'background_sync_prefix', $cycleData['prefix']);
  178. $this->config->setAppValue('user_ldap', 'background_sync_offset', $cycleData['offset']);
  179. }
  180. /**
  181. * returns data about the next cycle that should run, if any, otherwise
  182. * null. It also always goes for the next LDAP configuration!
  183. *
  184. * @param array|null $cycleData the old cycle
  185. * @return array|null
  186. */
  187. public function determineNextCycle(?array $cycleData = null) {
  188. $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true);
  189. if (count($prefixes) === 0) {
  190. return null;
  191. }
  192. // get the next prefix in line and remember it
  193. $oldPrefix = $cycleData === null ? null : $cycleData['prefix'];
  194. $prefix = $this->getNextPrefix($oldPrefix);
  195. if ($prefix === null) {
  196. return null;
  197. }
  198. $cycleData['prefix'] = $prefix;
  199. $cycleData['offset'] = 0;
  200. $this->setCycle(['prefix' => $prefix, 'offset' => 0]);
  201. return $cycleData;
  202. }
  203. /**
  204. * Checks whether the provided cycle should be run. Currently only the
  205. * last configuration change goes into account (at least one hour).
  206. *
  207. * @param $cycleData
  208. * @return bool
  209. */
  210. public function qualifiesToRun($cycleData) {
  211. $lastChange = (int)$this->config->getAppValue('user_ldap', $cycleData['prefix'] . '_lastChange', '0');
  212. if ((time() - $lastChange) > 60 * 30) {
  213. return true;
  214. }
  215. return false;
  216. }
  217. /**
  218. * increases the offset of the current cycle for the next run
  219. *
  220. * @param $cycleData
  221. */
  222. protected function increaseOffset($cycleData) {
  223. $ldapConfig = new Configuration($cycleData['prefix']);
  224. $cycleData['offset'] += (int)$ldapConfig->ldapPagingSize;
  225. $this->setCycle($cycleData);
  226. }
  227. /**
  228. * determines the next configuration prefix based on the last one (if any)
  229. *
  230. * @param string|null $lastPrefix
  231. * @return string|null
  232. */
  233. protected function getNextPrefix($lastPrefix) {
  234. $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true);
  235. $noOfPrefixes = count($prefixes);
  236. if ($noOfPrefixes === 0) {
  237. return null;
  238. }
  239. $i = $lastPrefix === null ? false : array_search($lastPrefix, $prefixes, true);
  240. if ($i === false) {
  241. $i = -1;
  242. } else {
  243. $i++;
  244. }
  245. if (!isset($prefixes[$i])) {
  246. $i = 0;
  247. }
  248. return $prefixes[$i];
  249. }
  250. /**
  251. * "fixes" DI
  252. */
  253. public function setArgument($argument) {
  254. if (isset($argument['config'])) {
  255. $this->config = $argument['config'];
  256. } else {
  257. $this->config = \OC::$server->getConfig();
  258. }
  259. if (isset($argument['helper'])) {
  260. $this->ldapHelper = $argument['helper'];
  261. } else {
  262. $this->ldapHelper = new Helper($this->config, \OC::$server->getDatabaseConnection());
  263. }
  264. if (isset($argument['ldapWrapper'])) {
  265. $this->ldap = $argument['ldapWrapper'];
  266. } else {
  267. $this->ldap = new LDAP($this->config->getSystemValueString('ldap_log_file'));
  268. }
  269. if (isset($argument['avatarManager'])) {
  270. $this->avatarManager = $argument['avatarManager'];
  271. } else {
  272. $this->avatarManager = \OC::$server->get(IAvatarManager::class);
  273. }
  274. if (isset($argument['dbc'])) {
  275. $this->dbc = $argument['dbc'];
  276. } else {
  277. $this->dbc = \OC::$server->getDatabaseConnection();
  278. }
  279. if (isset($argument['ncUserManager'])) {
  280. $this->ncUserManager = $argument['ncUserManager'];
  281. } else {
  282. $this->ncUserManager = \OC::$server->getUserManager();
  283. }
  284. if (isset($argument['logger'])) {
  285. $this->logger = $argument['logger'];
  286. } else {
  287. $this->logger = \OC::$server->get(LoggerInterface::class);
  288. }
  289. if (isset($argument['notificationManager'])) {
  290. $this->notificationManager = $argument['notificationManager'];
  291. } else {
  292. $this->notificationManager = \OC::$server->getNotificationManager();
  293. }
  294. if (isset($argument['mapper'])) {
  295. $this->mapper = $argument['mapper'];
  296. } else {
  297. $this->mapper = \OCP\Server::get(UserMapping::class);
  298. }
  299. if (isset($argument['connectionFactory'])) {
  300. $this->connectionFactory = $argument['connectionFactory'];
  301. } else {
  302. $this->connectionFactory = new ConnectionFactory($this->ldap);
  303. }
  304. if (isset($argument['accessFactory'])) {
  305. $this->accessFactory = $argument['accessFactory'];
  306. } else {
  307. $this->accessFactory = \OCP\Server::get(AccessFactory::class);
  308. }
  309. }
  310. }