Sync.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
  4. *
  5. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  6. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  7. * @author Joas Schilling <coding@schilljs.com>
  8. *
  9. * @license GNU AGPL version 3 or any later version
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU Affero General Public License as
  13. * published by the Free Software Foundation, either version 3 of the
  14. * License, or (at your option) any later version.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU Affero General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU Affero General Public License
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. *
  24. */
  25. namespace OCA\User_LDAP\Jobs;
  26. use OC\BackgroundJob\TimedJob;
  27. use OC\ServerNotAvailableException;
  28. use OCA\User_LDAP\AccessFactory;
  29. use OCA\User_LDAP\Configuration;
  30. use OCA\User_LDAP\ConnectionFactory;
  31. use OCA\User_LDAP\Helper;
  32. use OCA\User_LDAP\LDAP;
  33. use OCA\User_LDAP\Mapping\UserMapping;
  34. use OCA\User_LDAP\User\Manager;
  35. use OCP\IAvatarManager;
  36. use OCP\IConfig;
  37. use OCP\IDBConnection;
  38. use OCP\IUserManager;
  39. use OCP\Notification\IManager;
  40. use Psr\Log\LoggerInterface;
  41. class Sync extends TimedJob {
  42. public const MAX_INTERVAL = 12 * 60 * 60; // 12h
  43. public const MIN_INTERVAL = 30 * 60; // 30min
  44. /** @var Helper */
  45. protected $ldapHelper;
  46. /** @var LDAP */
  47. protected $ldap;
  48. /** @var Manager */
  49. protected $userManager;
  50. /** @var UserMapping */
  51. protected $mapper;
  52. /** @var IConfig */
  53. protected $config;
  54. /** @var IAvatarManager */
  55. protected $avatarManager;
  56. /** @var IDBConnection */
  57. protected $dbc;
  58. /** @var IUserManager */
  59. protected $ncUserManager;
  60. /** @var LoggerInterface */
  61. protected $logger;
  62. /** @var IManager */
  63. protected $notificationManager;
  64. /** @var ConnectionFactory */
  65. protected $connectionFactory;
  66. /** @var AccessFactory */
  67. protected $accessFactory;
  68. public function __construct(Manager $userManager) {
  69. $this->userManager = $userManager;
  70. $this->setInterval(
  71. \OC::$server->getConfig()->getAppValue(
  72. 'user_ldap',
  73. 'background_sync_interval',
  74. self::MIN_INTERVAL
  75. )
  76. );
  77. }
  78. /**
  79. * updates the interval
  80. *
  81. * the idea is to adjust the interval depending on the amount of known users
  82. * and the attempt to update each user one day. At most it would run every
  83. * 30 minutes, and at least every 12 hours.
  84. */
  85. public function updateInterval() {
  86. $minPagingSize = $this->getMinPagingSize();
  87. $mappedUsers = $this->mapper->count();
  88. $runsPerDay = ($minPagingSize === 0 || $mappedUsers === 0) ? self::MAX_INTERVAL
  89. : $mappedUsers / $minPagingSize;
  90. $interval = floor(24 * 60 * 60 / $runsPerDay);
  91. $interval = min(max($interval, self::MIN_INTERVAL), self::MAX_INTERVAL);
  92. $this->config->setAppValue('user_ldap', 'background_sync_interval', $interval);
  93. }
  94. /**
  95. * returns the smallest configured paging size
  96. * @return int
  97. */
  98. protected function getMinPagingSize() {
  99. $configKeys = $this->config->getAppKeys('user_ldap');
  100. $configKeys = array_filter($configKeys, function ($key) {
  101. return strpos($key, 'ldap_paging_size') !== false;
  102. });
  103. $minPagingSize = null;
  104. foreach ($configKeys as $configKey) {
  105. $pagingSize = $this->config->getAppValue('user_ldap', $configKey, $minPagingSize);
  106. $minPagingSize = $minPagingSize === null ? $pagingSize : min($minPagingSize, $pagingSize);
  107. }
  108. return (int)$minPagingSize;
  109. }
  110. /**
  111. * @param array $argument
  112. */
  113. public function run($argument) {
  114. $this->setArgument($argument);
  115. $isBackgroundJobModeAjax = $this->config
  116. ->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
  117. if ($isBackgroundJobModeAjax) {
  118. return;
  119. }
  120. $cycleData = $this->getCycle();
  121. if ($cycleData === null) {
  122. $cycleData = $this->determineNextCycle();
  123. if ($cycleData === null) {
  124. $this->updateInterval();
  125. return;
  126. }
  127. }
  128. if (!$this->qualifiesToRun($cycleData)) {
  129. $this->updateInterval();
  130. return;
  131. }
  132. try {
  133. $expectMoreResults = $this->runCycle($cycleData);
  134. if ($expectMoreResults) {
  135. $this->increaseOffset($cycleData);
  136. } else {
  137. $this->determineNextCycle($cycleData);
  138. }
  139. $this->updateInterval();
  140. } catch (ServerNotAvailableException $e) {
  141. $this->determineNextCycle($cycleData);
  142. }
  143. }
  144. /**
  145. * @param array $cycleData
  146. * @return bool whether more results are expected from the same configuration
  147. */
  148. public function runCycle($cycleData) {
  149. $connection = $this->connectionFactory->get($cycleData['prefix']);
  150. $access = $this->accessFactory->get($connection);
  151. $access->setUserMapper($this->mapper);
  152. $filter = $access->combineFilterWithAnd([
  153. $access->connection->ldapUserFilter,
  154. $access->connection->ldapUserDisplayName . '=*',
  155. $access->getFilterPartForUserSearch('')
  156. ]);
  157. $results = $access->fetchListOfUsers(
  158. $filter,
  159. $access->userManager->getAttributes(),
  160. $connection->ldapPagingSize,
  161. $cycleData['offset'],
  162. true
  163. );
  164. if ((int)$connection->ldapPagingSize === 0) {
  165. return false;
  166. }
  167. return count($results) >= (int)$connection->ldapPagingSize;
  168. }
  169. /**
  170. * returns the info about the current cycle that should be run, if any,
  171. * otherwise null
  172. *
  173. * @return array|null
  174. */
  175. public function getCycle() {
  176. $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true);
  177. if (count($prefixes) === 0) {
  178. return null;
  179. }
  180. $cycleData = [
  181. 'prefix' => $this->config->getAppValue('user_ldap', 'background_sync_prefix', null),
  182. 'offset' => (int)$this->config->getAppValue('user_ldap', 'background_sync_offset', 0),
  183. ];
  184. if (
  185. $cycleData['prefix'] !== null
  186. && in_array($cycleData['prefix'], $prefixes)
  187. ) {
  188. return $cycleData;
  189. }
  190. return null;
  191. }
  192. /**
  193. * Save the provided cycle information in the DB
  194. *
  195. * @param array $cycleData
  196. */
  197. public function setCycle(array $cycleData) {
  198. $this->config->setAppValue('user_ldap', 'background_sync_prefix', $cycleData['prefix']);
  199. $this->config->setAppValue('user_ldap', 'background_sync_offset', $cycleData['offset']);
  200. }
  201. /**
  202. * returns data about the next cycle that should run, if any, otherwise
  203. * null. It also always goes for the next LDAP configuration!
  204. *
  205. * @param array|null $cycleData the old cycle
  206. * @return array|null
  207. */
  208. public function determineNextCycle(array $cycleData = null) {
  209. $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true);
  210. if (count($prefixes) === 0) {
  211. return null;
  212. }
  213. // get the next prefix in line and remember it
  214. $oldPrefix = $cycleData === null ? null : $cycleData['prefix'];
  215. $prefix = $this->getNextPrefix($oldPrefix);
  216. if ($prefix === null) {
  217. return null;
  218. }
  219. $cycleData['prefix'] = $prefix;
  220. $cycleData['offset'] = 0;
  221. $this->setCycle(['prefix' => $prefix, 'offset' => 0]);
  222. return $cycleData;
  223. }
  224. /**
  225. * Checks whether the provided cycle should be run. Currently only the
  226. * last configuration change goes into account (at least one hour).
  227. *
  228. * @param $cycleData
  229. * @return bool
  230. */
  231. public function qualifiesToRun($cycleData) {
  232. $lastChange = $this->config->getAppValue('user_ldap', $cycleData['prefix'] . '_lastChange', 0);
  233. if ((time() - $lastChange) > 60 * 30) {
  234. return true;
  235. }
  236. return false;
  237. }
  238. /**
  239. * increases the offset of the current cycle for the next run
  240. *
  241. * @param $cycleData
  242. */
  243. protected function increaseOffset($cycleData) {
  244. $ldapConfig = new Configuration($cycleData['prefix']);
  245. $cycleData['offset'] += (int)$ldapConfig->ldapPagingSize;
  246. $this->setCycle($cycleData);
  247. }
  248. /**
  249. * determines the next configuration prefix based on the last one (if any)
  250. *
  251. * @param string|null $lastPrefix
  252. * @return string|null
  253. */
  254. protected function getNextPrefix($lastPrefix) {
  255. $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true);
  256. $noOfPrefixes = count($prefixes);
  257. if ($noOfPrefixes === 0) {
  258. return null;
  259. }
  260. $i = $lastPrefix === null ? false : array_search($lastPrefix, $prefixes, true);
  261. if ($i === false) {
  262. $i = -1;
  263. } else {
  264. $i++;
  265. }
  266. if (!isset($prefixes[$i])) {
  267. $i = 0;
  268. }
  269. return $prefixes[$i];
  270. }
  271. /**
  272. * "fixes" DI
  273. *
  274. * @param array $argument
  275. */
  276. public function setArgument($argument) {
  277. if (isset($argument['config'])) {
  278. $this->config = $argument['config'];
  279. } else {
  280. $this->config = \OC::$server->getConfig();
  281. }
  282. if (isset($argument['helper'])) {
  283. $this->ldapHelper = $argument['helper'];
  284. } else {
  285. $this->ldapHelper = new Helper($this->config, \OC::$server->getDatabaseConnection());
  286. }
  287. if (isset($argument['ldapWrapper'])) {
  288. $this->ldap = $argument['ldapWrapper'];
  289. } else {
  290. $this->ldap = new LDAP($this->config->getSystemValueString('ldap_log_file'));
  291. }
  292. if (isset($argument['avatarManager'])) {
  293. $this->avatarManager = $argument['avatarManager'];
  294. } else {
  295. $this->avatarManager = \OC::$server->getAvatarManager();
  296. }
  297. if (isset($argument['dbc'])) {
  298. $this->dbc = $argument['dbc'];
  299. } else {
  300. $this->dbc = \OC::$server->getDatabaseConnection();
  301. }
  302. if (isset($argument['ncUserManager'])) {
  303. $this->ncUserManager = $argument['ncUserManager'];
  304. } else {
  305. $this->ncUserManager = \OC::$server->getUserManager();
  306. }
  307. if (isset($argument['logger'])) {
  308. $this->logger = $argument['logger'];
  309. } else {
  310. $this->logger = \OC::$server->get(LoggerInterface::class);
  311. }
  312. if (isset($argument['notificationManager'])) {
  313. $this->notificationManager = $argument['notificationManager'];
  314. } else {
  315. $this->notificationManager = \OC::$server->getNotificationManager();
  316. }
  317. if (isset($argument['userManager'])) {
  318. $this->userManager = $argument['userManager'];
  319. }
  320. if (isset($argument['mapper'])) {
  321. $this->mapper = $argument['mapper'];
  322. } else {
  323. $this->mapper = \OC::$server->get(UserMapping::class);
  324. }
  325. if (isset($argument['connectionFactory'])) {
  326. $this->connectionFactory = $argument['connectionFactory'];
  327. } else {
  328. $this->connectionFactory = new ConnectionFactory($this->ldap);
  329. }
  330. if (isset($argument['accessFactory'])) {
  331. $this->accessFactory = $argument['accessFactory'];
  332. } else {
  333. $this->accessFactory = new AccessFactory(
  334. $this->ldap,
  335. $this->userManager,
  336. $this->ldapHelper,
  337. $this->config,
  338. $this->ncUserManager,
  339. $this->logger
  340. );
  341. }
  342. }
  343. }