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