Sync.php 10 KB

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