You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Access.php 64KB

8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
11 years ago
11 years ago
LDAP Wizard Overhaul wizard refactor reimplement save spinners and cursor implement Port detector introduced detector queue, added base dn detector disable input fields when detectors are running introduce spinners for fields that are being updated by detector cache jq element objects consolidate processing of detector results in generic / abstract base class display notification if a detector discovered a problem don't run base dn detector if a base is configured reset detector queue on configuration switch implement functionality check and update of status indicator document ConfigModel jsdoc for controller and main view more documentation implement the user filter tab view so far the multiselects get initialized (not filled yet) and the mode can be switched. mode is also restored. reintroduce filter switch confirmation in admin XP mode new detector for user object classes. so we also load user object classes if necessary and are able to save and show the setting. multiselect trigger save actions now on close only show spinners automatically, when a detector is running 20k limit for object classes preselection test adjust wordings, fix grammar add group (for users tab) detector also includes wording fixes error presentation moved from detectors to view, where it belongs add info label to users page missing wording changes show effective LDAP filter in Assisted Mode add user filter detector implement count button for users and limit all count actions to 1001 for performance reasons make port field a bit bigger. not perfect though. do not detect port automatically implement login filter tab view only load features in assisted mode and don't enable assisted fields while in raw mode add tooltips on login filter checkbox options for better understanding permanently show filter on login tab and also compile login filter in assisted mode test/verify button on login attributes tab, with backend changes. only run wizard requests if your an active tab. also run compile filter requests when switching to assisted mode underline toggle filter links to stress that they are clickable unity user and group tab functionality in common abstract class, add group filter tab view. only detectors and template adjustments left to have group tab implementation complete add object class and group detector for groups as well as filter composer show ldap filter permanently on groups tab introduce input element that can deal better with many groups, will be used with > 40 fix disabling complex group chooser while detection is running hide complex group chooser on config switch fix few more issues with complex chooser make complex group chooser available on Users tab as well detect base dn improvements/changes: - do not look for Base DN automatically, offer a button instead - fix for alternative way to detect a base dn (if agent dn is not given) - do not trigger filter composers on config switch Changes with configuration chooser controls - "New" was removed out of the configuration list - and split into buttons "add" and "copy" - delete button is also now an icon add test button for Base DN reimplement advanced tab. The save button is gone. reimplement expert tab remove unused methods implement mail attribute detector implement user display name attribute detection implement member group association detector replace text input with textarea for raw filter input finish functionality check auto-enable good configurations, as it was before cleanup move save confirmation handling to base class, reduces code duplication enable tabs only if no running save processes are left. move onConfigLoaded to base class, avoids code duplication simplify, save LOCs Test Configuration button to be dealt with in main view as it is a cross-tab element require detectorQueue in constructor cleanup put bootstrap into a function and thus make it testable get rid of old stuff
9 years ago
LDAP Wizard Overhaul wizard refactor reimplement save spinners and cursor implement Port detector introduced detector queue, added base dn detector disable input fields when detectors are running introduce spinners for fields that are being updated by detector cache jq element objects consolidate processing of detector results in generic / abstract base class display notification if a detector discovered a problem don't run base dn detector if a base is configured reset detector queue on configuration switch implement functionality check and update of status indicator document ConfigModel jsdoc for controller and main view more documentation implement the user filter tab view so far the multiselects get initialized (not filled yet) and the mode can be switched. mode is also restored. reintroduce filter switch confirmation in admin XP mode new detector for user object classes. so we also load user object classes if necessary and are able to save and show the setting. multiselect trigger save actions now on close only show spinners automatically, when a detector is running 20k limit for object classes preselection test adjust wordings, fix grammar add group (for users tab) detector also includes wording fixes error presentation moved from detectors to view, where it belongs add info label to users page missing wording changes show effective LDAP filter in Assisted Mode add user filter detector implement count button for users and limit all count actions to 1001 for performance reasons make port field a bit bigger. not perfect though. do not detect port automatically implement login filter tab view only load features in assisted mode and don't enable assisted fields while in raw mode add tooltips on login filter checkbox options for better understanding permanently show filter on login tab and also compile login filter in assisted mode test/verify button on login attributes tab, with backend changes. only run wizard requests if your an active tab. also run compile filter requests when switching to assisted mode underline toggle filter links to stress that they are clickable unity user and group tab functionality in common abstract class, add group filter tab view. only detectors and template adjustments left to have group tab implementation complete add object class and group detector for groups as well as filter composer show ldap filter permanently on groups tab introduce input element that can deal better with many groups, will be used with > 40 fix disabling complex group chooser while detection is running hide complex group chooser on config switch fix few more issues with complex chooser make complex group chooser available on Users tab as well detect base dn improvements/changes: - do not look for Base DN automatically, offer a button instead - fix for alternative way to detect a base dn (if agent dn is not given) - do not trigger filter composers on config switch Changes with configuration chooser controls - "New" was removed out of the configuration list - and split into buttons "add" and "copy" - delete button is also now an icon add test button for Base DN reimplement advanced tab. The save button is gone. reimplement expert tab remove unused methods implement mail attribute detector implement user display name attribute detection implement member group association detector replace text input with textarea for raw filter input finish functionality check auto-enable good configurations, as it was before cleanup move save confirmation handling to base class, reduces code duplication enable tabs only if no running save processes are left. move onConfigLoaded to base class, avoids code duplication simplify, save LOCs Test Configuration button to be dealt with in main view as it is a cross-tab element require detectorQueue in constructor cleanup put bootstrap into a function and thus make it testable get rid of old stuff
9 years ago
LDAP Wizard Overhaul wizard refactor reimplement save spinners and cursor implement Port detector introduced detector queue, added base dn detector disable input fields when detectors are running introduce spinners for fields that are being updated by detector cache jq element objects consolidate processing of detector results in generic / abstract base class display notification if a detector discovered a problem don't run base dn detector if a base is configured reset detector queue on configuration switch implement functionality check and update of status indicator document ConfigModel jsdoc for controller and main view more documentation implement the user filter tab view so far the multiselects get initialized (not filled yet) and the mode can be switched. mode is also restored. reintroduce filter switch confirmation in admin XP mode new detector for user object classes. so we also load user object classes if necessary and are able to save and show the setting. multiselect trigger save actions now on close only show spinners automatically, when a detector is running 20k limit for object classes preselection test adjust wordings, fix grammar add group (for users tab) detector also includes wording fixes error presentation moved from detectors to view, where it belongs add info label to users page missing wording changes show effective LDAP filter in Assisted Mode add user filter detector implement count button for users and limit all count actions to 1001 for performance reasons make port field a bit bigger. not perfect though. do not detect port automatically implement login filter tab view only load features in assisted mode and don't enable assisted fields while in raw mode add tooltips on login filter checkbox options for better understanding permanently show filter on login tab and also compile login filter in assisted mode test/verify button on login attributes tab, with backend changes. only run wizard requests if your an active tab. also run compile filter requests when switching to assisted mode underline toggle filter links to stress that they are clickable unity user and group tab functionality in common abstract class, add group filter tab view. only detectors and template adjustments left to have group tab implementation complete add object class and group detector for groups as well as filter composer show ldap filter permanently on groups tab introduce input element that can deal better with many groups, will be used with > 40 fix disabling complex group chooser while detection is running hide complex group chooser on config switch fix few more issues with complex chooser make complex group chooser available on Users tab as well detect base dn improvements/changes: - do not look for Base DN automatically, offer a button instead - fix for alternative way to detect a base dn (if agent dn is not given) - do not trigger filter composers on config switch Changes with configuration chooser controls - "New" was removed out of the configuration list - and split into buttons "add" and "copy" - delete button is also now an icon add test button for Base DN reimplement advanced tab. The save button is gone. reimplement expert tab remove unused methods implement mail attribute detector implement user display name attribute detection implement member group association detector replace text input with textarea for raw filter input finish functionality check auto-enable good configurations, as it was before cleanup move save confirmation handling to base class, reduces code duplication enable tabs only if no running save processes are left. move onConfigLoaded to base class, avoids code duplication simplify, save LOCs Test Configuration button to be dealt with in main view as it is a cross-tab element require detectorQueue in constructor cleanup put bootstrap into a function and thus make it testable get rid of old stuff
9 years ago
LDAP Wizard Overhaul wizard refactor reimplement save spinners and cursor implement Port detector introduced detector queue, added base dn detector disable input fields when detectors are running introduce spinners for fields that are being updated by detector cache jq element objects consolidate processing of detector results in generic / abstract base class display notification if a detector discovered a problem don't run base dn detector if a base is configured reset detector queue on configuration switch implement functionality check and update of status indicator document ConfigModel jsdoc for controller and main view more documentation implement the user filter tab view so far the multiselects get initialized (not filled yet) and the mode can be switched. mode is also restored. reintroduce filter switch confirmation in admin XP mode new detector for user object classes. so we also load user object classes if necessary and are able to save and show the setting. multiselect trigger save actions now on close only show spinners automatically, when a detector is running 20k limit for object classes preselection test adjust wordings, fix grammar add group (for users tab) detector also includes wording fixes error presentation moved from detectors to view, where it belongs add info label to users page missing wording changes show effective LDAP filter in Assisted Mode add user filter detector implement count button for users and limit all count actions to 1001 for performance reasons make port field a bit bigger. not perfect though. do not detect port automatically implement login filter tab view only load features in assisted mode and don't enable assisted fields while in raw mode add tooltips on login filter checkbox options for better understanding permanently show filter on login tab and also compile login filter in assisted mode test/verify button on login attributes tab, with backend changes. only run wizard requests if your an active tab. also run compile filter requests when switching to assisted mode underline toggle filter links to stress that they are clickable unity user and group tab functionality in common abstract class, add group filter tab view. only detectors and template adjustments left to have group tab implementation complete add object class and group detector for groups as well as filter composer show ldap filter permanently on groups tab introduce input element that can deal better with many groups, will be used with > 40 fix disabling complex group chooser while detection is running hide complex group chooser on config switch fix few more issues with complex chooser make complex group chooser available on Users tab as well detect base dn improvements/changes: - do not look for Base DN automatically, offer a button instead - fix for alternative way to detect a base dn (if agent dn is not given) - do not trigger filter composers on config switch Changes with configuration chooser controls - "New" was removed out of the configuration list - and split into buttons "add" and "copy" - delete button is also now an icon add test button for Base DN reimplement advanced tab. The save button is gone. reimplement expert tab remove unused methods implement mail attribute detector implement user display name attribute detection implement member group association detector replace text input with textarea for raw filter input finish functionality check auto-enable good configurations, as it was before cleanup move save confirmation handling to base class, reduces code duplication enable tabs only if no running save processes are left. move onConfigLoaded to base class, avoids code duplication simplify, save LOCs Test Configuration button to be dealt with in main view as it is a cross-tab element require detectorQueue in constructor cleanup put bootstrap into a function and thus make it testable get rid of old stuff
9 years ago
LDAP Wizard Overhaul wizard refactor reimplement save spinners and cursor implement Port detector introduced detector queue, added base dn detector disable input fields when detectors are running introduce spinners for fields that are being updated by detector cache jq element objects consolidate processing of detector results in generic / abstract base class display notification if a detector discovered a problem don't run base dn detector if a base is configured reset detector queue on configuration switch implement functionality check and update of status indicator document ConfigModel jsdoc for controller and main view more documentation implement the user filter tab view so far the multiselects get initialized (not filled yet) and the mode can be switched. mode is also restored. reintroduce filter switch confirmation in admin XP mode new detector for user object classes. so we also load user object classes if necessary and are able to save and show the setting. multiselect trigger save actions now on close only show spinners automatically, when a detector is running 20k limit for object classes preselection test adjust wordings, fix grammar add group (for users tab) detector also includes wording fixes error presentation moved from detectors to view, where it belongs add info label to users page missing wording changes show effective LDAP filter in Assisted Mode add user filter detector implement count button for users and limit all count actions to 1001 for performance reasons make port field a bit bigger. not perfect though. do not detect port automatically implement login filter tab view only load features in assisted mode and don't enable assisted fields while in raw mode add tooltips on login filter checkbox options for better understanding permanently show filter on login tab and also compile login filter in assisted mode test/verify button on login attributes tab, with backend changes. only run wizard requests if your an active tab. also run compile filter requests when switching to assisted mode underline toggle filter links to stress that they are clickable unity user and group tab functionality in common abstract class, add group filter tab view. only detectors and template adjustments left to have group tab implementation complete add object class and group detector for groups as well as filter composer show ldap filter permanently on groups tab introduce input element that can deal better with many groups, will be used with > 40 fix disabling complex group chooser while detection is running hide complex group chooser on config switch fix few more issues with complex chooser make complex group chooser available on Users tab as well detect base dn improvements/changes: - do not look for Base DN automatically, offer a button instead - fix for alternative way to detect a base dn (if agent dn is not given) - do not trigger filter composers on config switch Changes with configuration chooser controls - "New" was removed out of the configuration list - and split into buttons "add" and "copy" - delete button is also now an icon add test button for Base DN reimplement advanced tab. The save button is gone. reimplement expert tab remove unused methods implement mail attribute detector implement user display name attribute detection implement member group association detector replace text input with textarea for raw filter input finish functionality check auto-enable good configurations, as it was before cleanup move save confirmation handling to base class, reduces code duplication enable tabs only if no running save processes are left. move onConfigLoaded to base class, avoids code duplication simplify, save LOCs Test Configuration button to be dealt with in main view as it is a cross-tab element require detectorQueue in constructor cleanup put bootstrap into a function and thus make it testable get rid of old stuff
9 years ago
LDAP Wizard Overhaul wizard refactor reimplement save spinners and cursor implement Port detector introduced detector queue, added base dn detector disable input fields when detectors are running introduce spinners for fields that are being updated by detector cache jq element objects consolidate processing of detector results in generic / abstract base class display notification if a detector discovered a problem don't run base dn detector if a base is configured reset detector queue on configuration switch implement functionality check and update of status indicator document ConfigModel jsdoc for controller and main view more documentation implement the user filter tab view so far the multiselects get initialized (not filled yet) and the mode can be switched. mode is also restored. reintroduce filter switch confirmation in admin XP mode new detector for user object classes. so we also load user object classes if necessary and are able to save and show the setting. multiselect trigger save actions now on close only show spinners automatically, when a detector is running 20k limit for object classes preselection test adjust wordings, fix grammar add group (for users tab) detector also includes wording fixes error presentation moved from detectors to view, where it belongs add info label to users page missing wording changes show effective LDAP filter in Assisted Mode add user filter detector implement count button for users and limit all count actions to 1001 for performance reasons make port field a bit bigger. not perfect though. do not detect port automatically implement login filter tab view only load features in assisted mode and don't enable assisted fields while in raw mode add tooltips on login filter checkbox options for better understanding permanently show filter on login tab and also compile login filter in assisted mode test/verify button on login attributes tab, with backend changes. only run wizard requests if your an active tab. also run compile filter requests when switching to assisted mode underline toggle filter links to stress that they are clickable unity user and group tab functionality in common abstract class, add group filter tab view. only detectors and template adjustments left to have group tab implementation complete add object class and group detector for groups as well as filter composer show ldap filter permanently on groups tab introduce input element that can deal better with many groups, will be used with > 40 fix disabling complex group chooser while detection is running hide complex group chooser on config switch fix few more issues with complex chooser make complex group chooser available on Users tab as well detect base dn improvements/changes: - do not look for Base DN automatically, offer a button instead - fix for alternative way to detect a base dn (if agent dn is not given) - do not trigger filter composers on config switch Changes with configuration chooser controls - "New" was removed out of the configuration list - and split into buttons "add" and "copy" - delete button is also now an icon add test button for Base DN reimplement advanced tab. The save button is gone. reimplement expert tab remove unused methods implement mail attribute detector implement user display name attribute detection implement member group association detector replace text input with textarea for raw filter input finish functionality check auto-enable good configurations, as it was before cleanup move save confirmation handling to base class, reduces code duplication enable tabs only if no running save processes are left. move onConfigLoaded to base class, avoids code duplication simplify, save LOCs Test Configuration button to be dealt with in main view as it is a cross-tab element require detectorQueue in constructor cleanup put bootstrap into a function and thus make it testable get rid of old stuff
9 years ago
12 years ago
12 years ago
11 years ago
11 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Aaron Wood <aaronjwood@gmail.com>
  6. * @author Andreas Fischer <bantu@owncloud.com>
  7. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  8. * @author Bart Visscher <bartv@thisnet.nl>
  9. * @author Benjamin Diele <benjamin@diele.be>
  10. * @author bline <scottbeck@gmail.com>
  11. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  12. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  13. * @author J0WI <J0WI@users.noreply.github.com>
  14. * @author Joas Schilling <coding@schilljs.com>
  15. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  16. * @author Juan Pablo Villafáñez <jvillafanez@solidgear.es>
  17. * @author Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>
  18. * @author Lukas Reschke <lukas@statuscode.ch>
  19. * @author Mario Kolling <mario.kolling@serpro.gov.br>
  20. * @author Max Kovalenko <mxss1998@yandex.ru>
  21. * @author Morris Jobke <hey@morrisjobke.de>
  22. * @author Nicolas Grekas <nicolas.grekas@gmail.com>
  23. * @author Peter Kubica <peter@kubica.ch>
  24. * @author Ralph Krimmel <rkrimme1@gwdg.de>
  25. * @author Robin McCorkell <robin@mccorkell.me.uk>
  26. * @author Roeland Jago Douma <roeland@famdouma.nl>
  27. * @author Roger Szabo <roger.szabo@web.de>
  28. * @author Roland Tapken <roland@bitarbeiter.net>
  29. * @author root <root@localhost.localdomain>
  30. * @author Victor Dubiniuk <dubiniuk@owncloud.com>
  31. *
  32. * @license AGPL-3.0
  33. *
  34. * This code is free software: you can redistribute it and/or modify
  35. * it under the terms of the GNU Affero General Public License, version 3,
  36. * as published by the Free Software Foundation.
  37. *
  38. * This program is distributed in the hope that it will be useful,
  39. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  40. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  41. * GNU Affero General Public License for more details.
  42. *
  43. * You should have received a copy of the GNU Affero General Public License, version 3,
  44. * along with this program. If not, see <http://www.gnu.org/licenses/>
  45. *
  46. */
  47. namespace OCA\User_LDAP;
  48. use DomainException;
  49. use OC\Hooks\PublicEmitter;
  50. use OC\ServerNotAvailableException;
  51. use OCA\User_LDAP\Exceptions\ConstraintViolationException;
  52. use OCA\User_LDAP\Exceptions\NoMoreResults;
  53. use OCA\User_LDAP\Mapping\AbstractMapping;
  54. use OCA\User_LDAP\User\Manager;
  55. use OCA\User_LDAP\User\OfflineUser;
  56. use OCP\HintException;
  57. use OCP\IConfig;
  58. use OCP\IUserManager;
  59. use Psr\Log\LoggerInterface;
  60. use function strlen;
  61. use function substr;
  62. /**
  63. * Class Access
  64. *
  65. * @package OCA\User_LDAP
  66. */
  67. class Access extends LDAPUtility {
  68. public const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid'];
  69. /** @var \OCA\User_LDAP\Connection */
  70. public $connection;
  71. /** @var Manager */
  72. public $userManager;
  73. /**
  74. * never ever check this var directly, always use getPagedSearchResultState
  75. * @var ?bool
  76. */
  77. protected $pagedSearchedSuccessful;
  78. /** @var ?AbstractMapping */
  79. protected $userMapper;
  80. /** @var ?AbstractMapping */
  81. protected $groupMapper;
  82. /**
  83. * @var \OCA\User_LDAP\Helper
  84. */
  85. private $helper;
  86. /** @var IConfig */
  87. private $config;
  88. /** @var IUserManager */
  89. private $ncUserManager;
  90. /** @var LoggerInterface */
  91. private $logger;
  92. private string $lastCookie = '';
  93. public function __construct(
  94. Connection $connection,
  95. ILDAPWrapper $ldap,
  96. Manager $userManager,
  97. Helper $helper,
  98. IConfig $config,
  99. IUserManager $ncUserManager,
  100. LoggerInterface $logger
  101. ) {
  102. parent::__construct($ldap);
  103. $this->connection = $connection;
  104. $this->userManager = $userManager;
  105. $this->userManager->setLdapAccess($this);
  106. $this->helper = $helper;
  107. $this->config = $config;
  108. $this->ncUserManager = $ncUserManager;
  109. $this->logger = $logger;
  110. }
  111. /**
  112. * sets the User Mapper
  113. */
  114. public function setUserMapper(AbstractMapping $mapper): void {
  115. $this->userMapper = $mapper;
  116. }
  117. /**
  118. * @throws \Exception
  119. */
  120. public function getUserMapper(): AbstractMapping {
  121. if (is_null($this->userMapper)) {
  122. throw new \Exception('UserMapper was not assigned to this Access instance.');
  123. }
  124. return $this->userMapper;
  125. }
  126. /**
  127. * sets the Group Mapper
  128. */
  129. public function setGroupMapper(AbstractMapping $mapper): void {
  130. $this->groupMapper = $mapper;
  131. }
  132. /**
  133. * returns the Group Mapper
  134. *
  135. * @throws \Exception
  136. */
  137. public function getGroupMapper(): AbstractMapping {
  138. if (is_null($this->groupMapper)) {
  139. throw new \Exception('GroupMapper was not assigned to this Access instance.');
  140. }
  141. return $this->groupMapper;
  142. }
  143. /**
  144. * @return bool
  145. */
  146. private function checkConnection() {
  147. return ($this->connection instanceof Connection);
  148. }
  149. /**
  150. * returns the Connection instance
  151. *
  152. * @return \OCA\User_LDAP\Connection
  153. */
  154. public function getConnection() {
  155. return $this->connection;
  156. }
  157. /**
  158. * reads a given attribute for an LDAP record identified by a DN
  159. *
  160. * @param string $dn the record in question
  161. * @param string $attr the attribute that shall be retrieved
  162. * if empty, just check the record's existence
  163. * @param string $filter
  164. * @return array|false an array of values on success or an empty
  165. * array if $attr is empty, false otherwise
  166. * @throws ServerNotAvailableException
  167. */
  168. public function readAttribute(string $dn, string $attr, string $filter = 'objectClass=*') {
  169. if (!$this->checkConnection()) {
  170. $this->logger->warning(
  171. 'No LDAP Connector assigned, access impossible for readAttribute.',
  172. ['app' => 'user_ldap']
  173. );
  174. return false;
  175. }
  176. $cr = $this->connection->getConnectionResource();
  177. if (!$this->ldap->isResource($cr)) {
  178. //LDAP not available
  179. $this->logger->debug('LDAP resource not available.', ['app' => 'user_ldap']);
  180. return false;
  181. }
  182. $attr = mb_strtolower($attr, 'UTF-8');
  183. // the actual read attribute later may contain parameters on a ranged
  184. // request, e.g. member;range=99-199. Depends on server reply.
  185. $attrToRead = $attr;
  186. $values = [];
  187. $isRangeRequest = false;
  188. do {
  189. $result = $this->executeRead($dn, $attrToRead, $filter);
  190. if (is_bool($result)) {
  191. // when an exists request was run and it was successful, an empty
  192. // array must be returned
  193. return $result ? [] : false;
  194. }
  195. if (!$isRangeRequest) {
  196. $values = $this->extractAttributeValuesFromResult($result, $attr);
  197. if (!empty($values)) {
  198. return $values;
  199. }
  200. }
  201. $isRangeRequest = false;
  202. $result = $this->extractRangeData($result, $attr);
  203. if (!empty($result)) {
  204. $normalizedResult = $this->extractAttributeValuesFromResult(
  205. [$attr => $result['values']],
  206. $attr
  207. );
  208. $values = array_merge($values, $normalizedResult);
  209. if ($result['rangeHigh'] === '*') {
  210. // when server replies with * as high range value, there are
  211. // no more results left
  212. return $values;
  213. } else {
  214. $low = $result['rangeHigh'] + 1;
  215. $attrToRead = $result['attributeName'] . ';range=' . $low . '-*';
  216. $isRangeRequest = true;
  217. }
  218. }
  219. } while ($isRangeRequest);
  220. $this->logger->debug('Requested attribute ' . $attr . ' not found for ' . $dn, ['app' => 'user_ldap']);
  221. return false;
  222. }
  223. /**
  224. * Runs an read operation against LDAP
  225. *
  226. * @return array|bool false if there was any error, true if an exists check
  227. * was performed and the requested DN found, array with the
  228. * returned data on a successful usual operation
  229. * @throws ServerNotAvailableException
  230. */
  231. public function executeRead(string $dn, string $attribute, string $filter) {
  232. $dn = $this->helper->DNasBaseParameter($dn);
  233. $rr = @$this->invokeLDAPMethod('read', $dn, $filter, [$attribute]);
  234. if (!$this->ldap->isResource($rr)) {
  235. if ($attribute !== '') {
  236. //do not throw this message on userExists check, irritates
  237. $this->logger->debug('readAttribute failed for DN ' . $dn, ['app' => 'user_ldap']);
  238. }
  239. //in case an error occurs , e.g. object does not exist
  240. return false;
  241. }
  242. if ($attribute === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $rr) === 1)) {
  243. $this->logger->debug('readAttribute: ' . $dn . ' found', ['app' => 'user_ldap']);
  244. return true;
  245. }
  246. $er = $this->invokeLDAPMethod('firstEntry', $rr);
  247. if (!$this->ldap->isResource($er)) {
  248. //did not match the filter, return false
  249. return false;
  250. }
  251. //LDAP attributes are not case sensitive
  252. $result = \OCP\Util::mb_array_change_key_case(
  253. $this->invokeLDAPMethod('getAttributes', $er), MB_CASE_LOWER, 'UTF-8');
  254. return $result;
  255. }
  256. /**
  257. * Normalizes a result grom getAttributes(), i.e. handles DNs and binary
  258. * data if present.
  259. *
  260. * @param array $result from ILDAPWrapper::getAttributes()
  261. * @param string $attribute the attribute name that was read
  262. * @return string[]
  263. */
  264. public function extractAttributeValuesFromResult($result, $attribute) {
  265. $values = [];
  266. if (isset($result[$attribute]) && $result[$attribute]['count'] > 0) {
  267. $lowercaseAttribute = strtolower($attribute);
  268. for ($i = 0; $i < $result[$attribute]['count']; $i++) {
  269. if ($this->resemblesDN($attribute)) {
  270. $values[] = $this->helper->sanitizeDN($result[$attribute][$i]);
  271. } elseif ($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') {
  272. $values[] = $this->convertObjectGUID2Str($result[$attribute][$i]);
  273. } else {
  274. $values[] = $result[$attribute][$i];
  275. }
  276. }
  277. }
  278. return $values;
  279. }
  280. /**
  281. * Attempts to find ranged data in a getAttribute results and extracts the
  282. * returned values as well as information on the range and full attribute
  283. * name for further processing.
  284. *
  285. * @param array $result from ILDAPWrapper::getAttributes()
  286. * @param string $attribute the attribute name that was read. Without ";range=…"
  287. * @return array If a range was detected with keys 'values', 'attributeName',
  288. * 'attributeFull' and 'rangeHigh', otherwise empty.
  289. */
  290. public function extractRangeData($result, $attribute) {
  291. $keys = array_keys($result);
  292. foreach ($keys as $key) {
  293. if ($key !== $attribute && str_starts_with((string)$key, $attribute)) {
  294. $queryData = explode(';', (string)$key);
  295. if (str_starts_with($queryData[1], 'range=')) {
  296. $high = substr($queryData[1], 1 + strpos($queryData[1], '-'));
  297. $data = [
  298. 'values' => $result[$key],
  299. 'attributeName' => $queryData[0],
  300. 'attributeFull' => $key,
  301. 'rangeHigh' => $high,
  302. ];
  303. return $data;
  304. }
  305. }
  306. }
  307. return [];
  308. }
  309. /**
  310. * Set password for an LDAP user identified by a DN
  311. *
  312. * @param string $userDN the user in question
  313. * @param string $password the new password
  314. * @return bool
  315. * @throws HintException
  316. * @throws \Exception
  317. */
  318. public function setPassword($userDN, $password) {
  319. if ((int)$this->connection->turnOnPasswordChange !== 1) {
  320. throw new \Exception('LDAP password changes are disabled.');
  321. }
  322. $cr = $this->connection->getConnectionResource();
  323. if (!$this->ldap->isResource($cr)) {
  324. //LDAP not available
  325. $this->logger->debug('LDAP resource not available.', ['app' => 'user_ldap']);
  326. return false;
  327. }
  328. try {
  329. // try PASSWD extended operation first
  330. return @$this->invokeLDAPMethod('exopPasswd', $userDN, '', $password) ||
  331. @$this->invokeLDAPMethod('modReplace', $userDN, $password);
  332. } catch (ConstraintViolationException $e) {
  333. throw new HintException('Password change rejected.', \OCP\Util::getL10N('user_ldap')->t('Password change rejected. Hint: ') . $e->getMessage(), (int)$e->getCode());
  334. }
  335. }
  336. /**
  337. * checks whether the given attributes value is probably a DN
  338. *
  339. * @param string $attr the attribute in question
  340. * @return boolean if so true, otherwise false
  341. */
  342. private function resemblesDN($attr) {
  343. $resemblingAttributes = [
  344. 'dn',
  345. 'uniquemember',
  346. 'member',
  347. // memberOf is an "operational" attribute, without a definition in any RFC
  348. 'memberof'
  349. ];
  350. return in_array($attr, $resemblingAttributes);
  351. }
  352. /**
  353. * checks whether the given string is probably a DN
  354. *
  355. * @param string $string
  356. * @return boolean
  357. */
  358. public function stringResemblesDN($string) {
  359. $r = $this->ldap->explodeDN($string, 0);
  360. // if exploding a DN succeeds and does not end up in
  361. // an empty array except for $r[count] being 0.
  362. return (is_array($r) && count($r) > 1);
  363. }
  364. /**
  365. * returns a DN-string that is cleaned from not domain parts, e.g.
  366. * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
  367. * becomes dc=foobar,dc=server,dc=org
  368. *
  369. * @param string $dn
  370. * @return string
  371. */
  372. public function getDomainDNFromDN($dn) {
  373. $allParts = $this->ldap->explodeDN($dn, 0);
  374. if ($allParts === false) {
  375. //not a valid DN
  376. return '';
  377. }
  378. $domainParts = [];
  379. $dcFound = false;
  380. foreach ($allParts as $part) {
  381. if (!$dcFound && str_starts_with($part, 'dc=')) {
  382. $dcFound = true;
  383. }
  384. if ($dcFound) {
  385. $domainParts[] = $part;
  386. }
  387. }
  388. return implode(',', $domainParts);
  389. }
  390. /**
  391. * returns the LDAP DN for the given internal Nextcloud name of the group
  392. *
  393. * @param string $name the Nextcloud name in question
  394. * @return string|false LDAP DN on success, otherwise false
  395. */
  396. public function groupname2dn($name) {
  397. return $this->getGroupMapper()->getDNByName($name);
  398. }
  399. /**
  400. * returns the LDAP DN for the given internal Nextcloud name of the user
  401. *
  402. * @param string $name the Nextcloud name in question
  403. * @return string|false with the LDAP DN on success, otherwise false
  404. */
  405. public function username2dn($name) {
  406. $fdn = $this->getUserMapper()->getDNByName($name);
  407. //Check whether the DN belongs to the Base, to avoid issues on multi-
  408. //server setups
  409. if (is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
  410. return $fdn;
  411. }
  412. return false;
  413. }
  414. /**
  415. * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
  416. *
  417. * @param string $fdn the dn of the group object
  418. * @param string $ldapName optional, the display name of the object
  419. * @return string|false with the name to use in Nextcloud, false on DN outside of search DN
  420. * @throws \Exception
  421. */
  422. public function dn2groupname($fdn, $ldapName = null) {
  423. //To avoid bypassing the base DN settings under certain circumstances
  424. //with the group support, check whether the provided DN matches one of
  425. //the given Bases
  426. if (!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
  427. return false;
  428. }
  429. return $this->dn2ocname($fdn, $ldapName, false);
  430. }
  431. /**
  432. * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
  433. *
  434. * @param string $fdn the dn of the user object
  435. * @param string $ldapName optional, the display name of the object
  436. * @return string|false with with the name to use in Nextcloud
  437. * @throws \Exception
  438. */
  439. public function dn2username($fdn, $ldapName = null) {
  440. //To avoid bypassing the base DN settings under certain circumstances
  441. //with the group support, check whether the provided DN matches one of
  442. //the given Bases
  443. if (!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
  444. return false;
  445. }
  446. return $this->dn2ocname($fdn, $ldapName, true);
  447. }
  448. /**
  449. * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN
  450. *
  451. * @param string $fdn the dn of the user object
  452. * @param string|null $ldapName optional, the display name of the object
  453. * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
  454. * @param bool|null $newlyMapped
  455. * @param array|null $record
  456. * @return false|string with with the name to use in Nextcloud
  457. * @throws \Exception
  458. */
  459. public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, ?array $record = null) {
  460. static $intermediates = [];
  461. if (isset($intermediates[($isUser ? 'user-' : 'group-') . $fdn])) {
  462. return false; // is a known intermediate
  463. }
  464. $newlyMapped = false;
  465. if ($isUser) {
  466. $mapper = $this->getUserMapper();
  467. $nameAttribute = $this->connection->ldapUserDisplayName;
  468. $filter = $this->connection->ldapUserFilter;
  469. } else {
  470. $mapper = $this->getGroupMapper();
  471. $nameAttribute = $this->connection->ldapGroupDisplayName;
  472. $filter = $this->connection->ldapGroupFilter;
  473. }
  474. //let's try to retrieve the Nextcloud name from the mappings table
  475. $ncName = $mapper->getNameByDN($fdn);
  476. if (is_string($ncName)) {
  477. return $ncName;
  478. }
  479. //second try: get the UUID and check if it is known. Then, update the DN and return the name.
  480. $uuid = $this->getUUID($fdn, $isUser, $record);
  481. if (is_string($uuid)) {
  482. $ncName = $mapper->getNameByUUID($uuid);
  483. if (is_string($ncName)) {
  484. $mapper->setDNbyUUID($fdn, $uuid);
  485. return $ncName;
  486. }
  487. } else {
  488. //If the UUID can't be detected something is foul.
  489. $this->logger->debug('Cannot determine UUID for ' . $fdn . '. Skipping.', ['app' => 'user_ldap']);
  490. return false;
  491. }
  492. if (is_null($ldapName)) {
  493. $ldapName = $this->readAttribute($fdn, $nameAttribute, $filter);
  494. if (!isset($ldapName[0]) || empty($ldapName[0])) {
  495. $this->logger->debug('No or empty name for ' . $fdn . ' with filter ' . $filter . '.', ['app' => 'user_ldap']);
  496. $intermediates[($isUser ? 'user-' : 'group-') . $fdn] = true;
  497. return false;
  498. }
  499. $ldapName = $ldapName[0];
  500. }
  501. if ($isUser) {
  502. $usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr;
  503. if ($usernameAttribute !== '') {
  504. $username = $this->readAttribute($fdn, $usernameAttribute);
  505. if (!isset($username[0]) || empty($username[0])) {
  506. $this->logger->debug('No or empty username (' . $usernameAttribute . ') for ' . $fdn . '.', ['app' => 'user_ldap']);
  507. return false;
  508. }
  509. $username = $username[0];
  510. } else {
  511. $username = $uuid;
  512. }
  513. try {
  514. $intName = $this->sanitizeUsername($username);
  515. } catch (\InvalidArgumentException $e) {
  516. $this->logger->warning('Error sanitizing username: ' . $e->getMessage(), [
  517. 'exception' => $e,
  518. ]);
  519. // we don't attempt to set a username here. We can go for
  520. // for an alternative 4 digit random number as we would append
  521. // otherwise, however it's likely not enough space in bigger
  522. // setups, and most importantly: this is not intended.
  523. return false;
  524. }
  525. } else {
  526. $intName = $this->sanitizeGroupIDCandidate($ldapName);
  527. }
  528. //a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
  529. //disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
  530. //NOTE: mind, disabling cache affects only this instance! Using it
  531. // outside of core user management will still cache the user as non-existing.
  532. $originalTTL = $this->connection->ldapCacheTTL;
  533. $this->connection->setConfiguration(['ldapCacheTTL' => 0]);
  534. if ($intName !== ''
  535. && (($isUser && !$this->ncUserManager->userExists($intName))
  536. || (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))
  537. )
  538. ) {
  539. $this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
  540. $newlyMapped = $this->mapAndAnnounceIfApplicable($mapper, $fdn, $intName, $uuid, $isUser);
  541. if ($newlyMapped) {
  542. return $intName;
  543. }
  544. }
  545. $this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
  546. $altName = $this->createAltInternalOwnCloudName($intName, $isUser);
  547. if (is_string($altName)) {
  548. if ($this->mapAndAnnounceIfApplicable($mapper, $fdn, $altName, $uuid, $isUser)) {
  549. $this->logger->warning(
  550. 'Mapped {fdn} as {altName} because of a name collision on {intName}.',
  551. [
  552. 'fdn' => $fdn,
  553. 'altName' => $altName,
  554. 'intName' => $intName,
  555. 'app' => 'user_ldap',
  556. ]
  557. );
  558. $newlyMapped = true;
  559. return $altName;
  560. }
  561. }
  562. //if everything else did not help..
  563. $this->logger->info('Could not create unique name for ' . $fdn . '.', ['app' => 'user_ldap']);
  564. return false;
  565. }
  566. public function mapAndAnnounceIfApplicable(
  567. AbstractMapping $mapper,
  568. string $fdn,
  569. string $name,
  570. string $uuid,
  571. bool $isUser
  572. ): bool {
  573. if ($mapper->map($fdn, $name, $uuid)) {
  574. if ($this->ncUserManager instanceof PublicEmitter && $isUser) {
  575. $this->cacheUserExists($name);
  576. $this->ncUserManager->emit('\OC\User', 'assignedUserId', [$name]);
  577. } elseif (!$isUser) {
  578. $this->cacheGroupExists($name);
  579. }
  580. return true;
  581. }
  582. return false;
  583. }
  584. /**
  585. * gives back the user names as they are used ownClod internally
  586. *
  587. * @param array $ldapUsers as returned by fetchList()
  588. * @return array<int,string> an array with the user names to use in Nextcloud
  589. *
  590. * gives back the user names as they are used ownClod internally
  591. * @throws \Exception
  592. */
  593. public function nextcloudUserNames($ldapUsers) {
  594. return $this->ldap2NextcloudNames($ldapUsers, true);
  595. }
  596. /**
  597. * gives back the group names as they are used ownClod internally
  598. *
  599. * @param array $ldapGroups as returned by fetchList()
  600. * @return array<int,string> an array with the group names to use in Nextcloud
  601. *
  602. * gives back the group names as they are used ownClod internally
  603. * @throws \Exception
  604. */
  605. public function nextcloudGroupNames($ldapGroups) {
  606. return $this->ldap2NextcloudNames($ldapGroups, false);
  607. }
  608. /**
  609. * @param array[] $ldapObjects as returned by fetchList()
  610. * @return array<int,string>
  611. * @throws \Exception
  612. */
  613. private function ldap2NextcloudNames(array $ldapObjects, bool $isUsers): array {
  614. if ($isUsers) {
  615. $nameAttribute = $this->connection->ldapUserDisplayName;
  616. $sndAttribute = $this->connection->ldapUserDisplayName2;
  617. } else {
  618. $nameAttribute = $this->connection->ldapGroupDisplayName;
  619. $sndAttribute = null;
  620. }
  621. $nextcloudNames = [];
  622. foreach ($ldapObjects as $ldapObject) {
  623. $nameByLDAP = $ldapObject[$nameAttribute][0] ?? null;
  624. $ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
  625. if ($ncName) {
  626. $nextcloudNames[] = $ncName;
  627. if ($isUsers) {
  628. $this->updateUserState($ncName);
  629. //cache the user names so it does not need to be retrieved
  630. //again later (e.g. sharing dialogue).
  631. if (is_null($nameByLDAP)) {
  632. continue;
  633. }
  634. $sndName = $ldapObject[$sndAttribute][0] ?? '';
  635. $this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
  636. } elseif ($nameByLDAP !== null) {
  637. $this->cacheGroupDisplayName($ncName, $nameByLDAP);
  638. }
  639. }
  640. }
  641. return $nextcloudNames;
  642. }
  643. /**
  644. * removes the deleted-flag of a user if it was set
  645. *
  646. * @param string $ncname
  647. * @throws \Exception
  648. */
  649. public function updateUserState($ncname): void {
  650. $user = $this->userManager->get($ncname);
  651. if ($user instanceof OfflineUser) {
  652. $user->unmark();
  653. }
  654. }
  655. /**
  656. * caches the user display name
  657. *
  658. * @param string $ocName the internal Nextcloud username
  659. * @param string|false $home the home directory path
  660. */
  661. public function cacheUserHome(string $ocName, $home): void {
  662. $cacheKey = 'getHome' . $ocName;
  663. $this->connection->writeToCache($cacheKey, $home);
  664. }
  665. /**
  666. * caches a user as existing
  667. */
  668. public function cacheUserExists(string $ocName): void {
  669. $this->connection->writeToCache('userExists' . $ocName, true);
  670. }
  671. /**
  672. * caches a group as existing
  673. */
  674. public function cacheGroupExists(string $gid): void {
  675. $this->connection->writeToCache('groupExists' . $gid, true);
  676. }
  677. /**
  678. * caches the user display name
  679. *
  680. * @param string $ocName the internal Nextcloud username
  681. * @param string $displayName the display name
  682. * @param string $displayName2 the second display name
  683. * @throws \Exception
  684. */
  685. public function cacheUserDisplayName(string $ocName, string $displayName, string $displayName2 = ''): void {
  686. $user = $this->userManager->get($ocName);
  687. if ($user === null) {
  688. return;
  689. }
  690. $displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
  691. $cacheKeyTrunk = 'getDisplayName';
  692. $this->connection->writeToCache($cacheKeyTrunk . $ocName, $displayName);
  693. }
  694. public function cacheGroupDisplayName(string $ncName, string $displayName): void {
  695. $cacheKey = 'group_getDisplayName' . $ncName;
  696. $this->connection->writeToCache($cacheKey, $displayName);
  697. }
  698. /**
  699. * creates a unique name for internal Nextcloud use for users. Don't call it directly.
  700. *
  701. * @param string $name the display name of the object
  702. * @return string|false with with the name to use in Nextcloud or false if unsuccessful
  703. *
  704. * Instead of using this method directly, call
  705. * createAltInternalOwnCloudName($name, true)
  706. */
  707. private function _createAltInternalOwnCloudNameForUsers(string $name) {
  708. $attempts = 0;
  709. //while loop is just a precaution. If a name is not generated within
  710. //20 attempts, something else is very wrong. Avoids infinite loop.
  711. while ($attempts < 20) {
  712. $altName = $name . '_' . rand(1000, 9999);
  713. if (!$this->ncUserManager->userExists($altName)) {
  714. return $altName;
  715. }
  716. $attempts++;
  717. }
  718. return false;
  719. }
  720. /**
  721. * creates a unique name for internal Nextcloud use for groups. Don't call it directly.
  722. *
  723. * @param string $name the display name of the object
  724. * @return string|false with with the name to use in Nextcloud or false if unsuccessful.
  725. *
  726. * Instead of using this method directly, call
  727. * createAltInternalOwnCloudName($name, false)
  728. *
  729. * Group names are also used as display names, so we do a sequential
  730. * numbering, e.g. Developers_42 when there are 41 other groups called
  731. * "Developers"
  732. */
  733. private function _createAltInternalOwnCloudNameForGroups(string $name) {
  734. $usedNames = $this->getGroupMapper()->getNamesBySearch($name, "", '_%');
  735. if (count($usedNames) === 0) {
  736. $lastNo = 1; //will become name_2
  737. } else {
  738. natsort($usedNames);
  739. $lastName = array_pop($usedNames);
  740. $lastNo = (int)substr($lastName, strrpos($lastName, '_') + 1);
  741. }
  742. $altName = $name . '_' . (string)($lastNo + 1);
  743. unset($usedNames);
  744. $attempts = 1;
  745. while ($attempts < 21) {
  746. // Check to be really sure it is unique
  747. // while loop is just a precaution. If a name is not generated within
  748. // 20 attempts, something else is very wrong. Avoids infinite loop.
  749. if (!\OC::$server->getGroupManager()->groupExists($altName)) {
  750. return $altName;
  751. }
  752. $altName = $name . '_' . ($lastNo + $attempts);
  753. $attempts++;
  754. }
  755. return false;
  756. }
  757. /**
  758. * creates a unique name for internal Nextcloud use.
  759. *
  760. * @param string $name the display name of the object
  761. * @param bool $isUser whether name should be created for a user (true) or a group (false)
  762. * @return string|false with with the name to use in Nextcloud or false if unsuccessful
  763. */
  764. private function createAltInternalOwnCloudName(string $name, bool $isUser) {
  765. // ensure there is space for the "_1234" suffix
  766. if (strlen($name) > 59) {
  767. $name = substr($name, 0, 59);
  768. }
  769. $originalTTL = $this->connection->ldapCacheTTL;
  770. $this->connection->setConfiguration(['ldapCacheTTL' => 0]);
  771. if ($isUser) {
  772. $altName = $this->_createAltInternalOwnCloudNameForUsers($name);
  773. } else {
  774. $altName = $this->_createAltInternalOwnCloudNameForGroups($name);
  775. }
  776. $this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
  777. return $altName;
  778. }
  779. /**
  780. * fetches a list of users according to a provided loginName and utilizing
  781. * the login filter.
  782. */
  783. public function fetchUsersByLoginName(string $loginName, array $attributes = ['dn']): array {
  784. $loginName = $this->escapeFilterPart($loginName);
  785. $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
  786. return $this->fetchListOfUsers($filter, $attributes);
  787. }
  788. /**
  789. * counts the number of users according to a provided loginName and
  790. * utilizing the login filter.
  791. *
  792. * @param string $loginName
  793. * @return false|int
  794. */
  795. public function countUsersByLoginName($loginName) {
  796. $loginName = $this->escapeFilterPart($loginName);
  797. $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
  798. return $this->countUsers($filter);
  799. }
  800. /**
  801. * @throws \Exception
  802. */
  803. public function fetchListOfUsers(string $filter, array $attr, ?int $limit = null, ?int $offset = null, bool $forceApplyAttributes = false): array {
  804. $ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
  805. $recordsToUpdate = $ldapRecords;
  806. if (!$forceApplyAttributes) {
  807. $isBackgroundJobModeAjax = $this->config
  808. ->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
  809. $listOfDNs = array_reduce($ldapRecords, function ($listOfDNs, $entry) {
  810. $listOfDNs[] = $entry['dn'][0];
  811. return $listOfDNs;
  812. }, []);
  813. $idsByDn = $this->getUserMapper()->getListOfIdsByDn($listOfDNs);
  814. $recordsToUpdate = array_filter($ldapRecords, function ($record) use ($isBackgroundJobModeAjax, $idsByDn) {
  815. $newlyMapped = false;
  816. $uid = $idsByDn[$record['dn'][0]] ?? null;
  817. if ($uid === null) {
  818. $uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record);
  819. }
  820. if (is_string($uid)) {
  821. $this->cacheUserExists($uid);
  822. }
  823. return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax);
  824. });
  825. }
  826. $this->batchApplyUserAttributes($recordsToUpdate);
  827. return $this->fetchList($ldapRecords, $this->manyAttributes($attr));
  828. }
  829. /**
  830. * provided with an array of LDAP user records the method will fetch the
  831. * user object and requests it to process the freshly fetched attributes and
  832. * and their values
  833. *
  834. * @throws \Exception
  835. */
  836. public function batchApplyUserAttributes(array $ldapRecords): void {
  837. $displayNameAttribute = strtolower((string)$this->connection->ldapUserDisplayName);
  838. foreach ($ldapRecords as $userRecord) {
  839. if (!isset($userRecord[$displayNameAttribute])) {
  840. // displayName is obligatory
  841. continue;
  842. }
  843. $ocName = $this->dn2ocname($userRecord['dn'][0], null, true);
  844. if ($ocName === false) {
  845. continue;
  846. }
  847. $this->updateUserState($ocName);
  848. $user = $this->userManager->get($ocName);
  849. if ($user !== null) {
  850. $user->processAttributes($userRecord);
  851. } else {
  852. $this->logger->debug(
  853. "The ldap user manager returned null for $ocName",
  854. ['app' => 'user_ldap']
  855. );
  856. }
  857. }
  858. }
  859. /**
  860. * @return array[]
  861. */
  862. public function fetchListOfGroups(string $filter, array $attr, ?int $limit = null, ?int $offset = null): array {
  863. $cacheKey = 'fetchListOfGroups_' . $filter . '_' . implode('-', $attr) . '_' . (string)$limit . '_' . (string)$offset;
  864. $listOfGroups = $this->connection->getFromCache($cacheKey);
  865. if (!is_null($listOfGroups)) {
  866. return $listOfGroups;
  867. }
  868. $groupRecords = $this->searchGroups($filter, $attr, $limit, $offset);
  869. $listOfDNs = array_reduce($groupRecords, function ($listOfDNs, $entry) {
  870. $listOfDNs[] = $entry['dn'][0];
  871. return $listOfDNs;
  872. }, []);
  873. $idsByDn = $this->getGroupMapper()->getListOfIdsByDn($listOfDNs);
  874. array_walk($groupRecords, function (array $record) use ($idsByDn) {
  875. $newlyMapped = false;
  876. $gid = $idsByDn[$record['dn'][0]] ?? null;
  877. if ($gid === null) {
  878. $gid = $this->dn2ocname($record['dn'][0], null, false, $newlyMapped, $record);
  879. }
  880. if (!$newlyMapped && is_string($gid)) {
  881. $this->cacheGroupExists($gid);
  882. }
  883. });
  884. $listOfGroups = $this->fetchList($groupRecords, $this->manyAttributes($attr));
  885. $this->connection->writeToCache($cacheKey, $listOfGroups);
  886. return $listOfGroups;
  887. }
  888. private function fetchList(array $list, bool $manyAttributes): array {
  889. if ($manyAttributes) {
  890. return $list;
  891. } else {
  892. $list = array_reduce($list, function ($carry, $item) {
  893. $attribute = array_keys($item)[0];
  894. $carry[] = $item[$attribute][0];
  895. return $carry;
  896. }, []);
  897. return array_unique($list, SORT_LOCALE_STRING);
  898. }
  899. }
  900. /**
  901. * @throws ServerNotAvailableException
  902. */
  903. public function searchUsers(string $filter, ?array $attr = null, ?int $limit = null, ?int $offset = null): array {
  904. $result = [];
  905. foreach ($this->connection->ldapBaseUsers as $base) {
  906. $result = array_merge($result, $this->search($filter, $base, $attr, $limit, $offset));
  907. }
  908. return $result;
  909. }
  910. /**
  911. * @param string[] $attr
  912. * @return false|int
  913. * @throws ServerNotAvailableException
  914. */
  915. public function countUsers(string $filter, array $attr = ['dn'], ?int $limit = null, ?int $offset = null) {
  916. $result = false;
  917. foreach ($this->connection->ldapBaseUsers as $base) {
  918. $count = $this->count($filter, [$base], $attr, $limit ?? 0, $offset ?? 0);
  919. $result = is_int($count) ? (int)$result + $count : $result;
  920. }
  921. return $result;
  922. }
  923. /**
  924. * executes an LDAP search, optimized for Groups
  925. *
  926. * @param ?string[] $attr optional, when certain attributes shall be filtered out
  927. *
  928. * Executes an LDAP search
  929. * @throws ServerNotAvailableException
  930. */
  931. public function searchGroups(string $filter, ?array $attr = null, ?int $limit = null, ?int $offset = null): array {
  932. $result = [];
  933. foreach ($this->connection->ldapBaseGroups as $base) {
  934. $result = array_merge($result, $this->search($filter, $base, $attr, $limit, $offset));
  935. }
  936. return $result;
  937. }
  938. /**
  939. * returns the number of available groups
  940. *
  941. * @return int|bool
  942. * @throws ServerNotAvailableException
  943. */
  944. public function countGroups(string $filter, array $attr = ['dn'], ?int $limit = null, ?int $offset = null) {
  945. $result = false;
  946. foreach ($this->connection->ldapBaseGroups as $base) {
  947. $count = $this->count($filter, [$base], $attr, $limit ?? 0, $offset ?? 0);
  948. $result = is_int($count) ? (int)$result + $count : $result;
  949. }
  950. return $result;
  951. }
  952. /**
  953. * returns the number of available objects on the base DN
  954. *
  955. * @return int|bool
  956. * @throws ServerNotAvailableException
  957. */
  958. public function countObjects(?int $limit = null, ?int $offset = null) {
  959. $result = false;
  960. foreach ($this->connection->ldapBase as $base) {
  961. $count = $this->count('objectclass=*', [$base], ['dn'], $limit ?? 0, $offset ?? 0);
  962. $result = is_int($count) ? (int)$result + $count : $result;
  963. }
  964. return $result;
  965. }
  966. /**
  967. * Returns the LDAP handler
  968. *
  969. * @throws \OC\ServerNotAvailableException
  970. */
  971. /**
  972. * @param mixed[] $arguments
  973. * @return mixed
  974. * @throws \OC\ServerNotAvailableException
  975. */
  976. private function invokeLDAPMethod(string $command, ...$arguments) {
  977. if ($command == 'controlPagedResultResponse') {
  978. // php no longer supports call-time pass-by-reference
  979. // thus cannot support controlPagedResultResponse as the third argument
  980. // is a reference
  981. throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
  982. }
  983. if (!method_exists($this->ldap, $command)) {
  984. return null;
  985. }
  986. array_unshift($arguments, $this->connection->getConnectionResource());
  987. $doMethod = function () use ($command, &$arguments) {
  988. return call_user_func_array([$this->ldap, $command], $arguments);
  989. };
  990. try {
  991. $ret = $doMethod();
  992. } catch (ServerNotAvailableException $e) {
  993. /* Server connection lost, attempt to reestablish it
  994. * Maybe implement exponential backoff?
  995. * This was enough to get solr indexer working which has large delays between LDAP fetches.
  996. */
  997. $this->logger->debug("Connection lost on $command, attempting to reestablish.", ['app' => 'user_ldap']);
  998. $this->connection->resetConnectionResource();
  999. $cr = $this->connection->getConnectionResource();
  1000. if (!$this->ldap->isResource($cr)) {
  1001. // Seems like we didn't find any resource.
  1002. $this->logger->debug("Could not $command, because resource is missing.", ['app' => 'user_ldap']);
  1003. throw $e;
  1004. }
  1005. $arguments[0] = $cr;
  1006. $ret = $doMethod();
  1007. }
  1008. return $ret;
  1009. }
  1010. /**
  1011. * retrieved. Results will according to the order in the array.
  1012. *
  1013. * @param string $filter
  1014. * @param string $base
  1015. * @param string[] $attr
  1016. * @param int|null $limit optional, maximum results to be counted
  1017. * @param int|null $offset optional, a starting point
  1018. * @return array|false array with the search result as first value and pagedSearchOK as
  1019. * second | false if not successful
  1020. * @throws ServerNotAvailableException
  1021. */
  1022. private function executeSearch(
  1023. string $filter,
  1024. string $base,
  1025. ?array &$attr,
  1026. ?int $pageSize,
  1027. ?int $offset
  1028. ) {
  1029. // See if we have a resource, in case not cancel with message
  1030. $cr = $this->connection->getConnectionResource();
  1031. if (!$this->ldap->isResource($cr)) {
  1032. // Seems like we didn't find any resource.
  1033. // Return an empty array just like before.
  1034. $this->logger->debug('Could not search, because resource is missing.', ['app' => 'user_ldap']);
  1035. return false;
  1036. }
  1037. //check whether paged search should be attempted
  1038. try {
  1039. [$pagedSearchOK, $pageSize, $cookie] = $this->initPagedSearch($filter, $base, $attr, (int)$pageSize, (int)$offset);
  1040. } catch (NoMoreResults $e) {
  1041. // beyond last results page
  1042. return false;
  1043. }
  1044. $sr = $this->invokeLDAPMethod('search', $base, $filter, $attr, 0, 0, $pageSize, $cookie);
  1045. $error = $this->ldap->errno($this->connection->getConnectionResource());
  1046. if (!$this->ldap->isResource($sr) || $error !== 0) {
  1047. $this->logger->error('Attempt for Paging? ' . print_r($pagedSearchOK, true), ['app' => 'user_ldap']);
  1048. return false;
  1049. }
  1050. return [$sr, $pagedSearchOK];
  1051. }
  1052. /**
  1053. * processes an LDAP paged search operation
  1054. *
  1055. * @param resource|\LDAP\Result|resource[]|\LDAP\Result[] $sr the array containing the LDAP search resources
  1056. * @param int $foundItems number of results in the single search operation
  1057. * @param int $limit maximum results to be counted
  1058. * @param bool $pagedSearchOK whether a paged search has been executed
  1059. * @param bool $skipHandling required for paged search when cookies to
  1060. * prior results need to be gained
  1061. * @return bool cookie validity, true if we have more pages, false otherwise.
  1062. * @throws ServerNotAvailableException
  1063. */
  1064. private function processPagedSearchStatus(
  1065. $sr,
  1066. int $foundItems,
  1067. int $limit,
  1068. bool $pagedSearchOK,
  1069. bool $skipHandling
  1070. ): bool {
  1071. $cookie = '';
  1072. if ($pagedSearchOK) {
  1073. $cr = $this->connection->getConnectionResource();
  1074. if ($this->ldap->controlPagedResultResponse($cr, $sr, $cookie)) {
  1075. $this->lastCookie = $cookie;
  1076. }
  1077. //browsing through prior pages to get the cookie for the new one
  1078. if ($skipHandling) {
  1079. return false;
  1080. }
  1081. // if count is bigger, then the server does not support
  1082. // paged search. Instead, he did a normal search. We set a
  1083. // flag here, so the callee knows how to deal with it.
  1084. if ($foundItems <= $limit) {
  1085. $this->pagedSearchedSuccessful = true;
  1086. }
  1087. } else {
  1088. if ((int)$this->connection->ldapPagingSize !== 0) {
  1089. $this->logger->debug(
  1090. 'Paged search was not available',
  1091. ['app' => 'user_ldap']
  1092. );
  1093. }
  1094. }
  1095. /* ++ Fixing RHDS searches with pages with zero results ++
  1096. * Return cookie status. If we don't have more pages, with RHDS
  1097. * cookie is null, with openldap cookie is an empty string and
  1098. * to 386ds '0' is a valid cookie. Even if $iFoundItems == 0
  1099. */
  1100. return !empty($cookie) || $cookie === '0';
  1101. }
  1102. /**
  1103. * executes an LDAP search, but counts the results only
  1104. *
  1105. * @param string $filter the LDAP filter for the search
  1106. * @param array $bases an array containing the LDAP subtree(s) that shall be searched
  1107. * @param ?string[] $attr optional, array, one or more attributes that shall be
  1108. * retrieved. Results will according to the order in the array.
  1109. * @param int $limit maximum results to be counted, 0 means no limit
  1110. * @param int $offset a starting point, defaults to 0
  1111. * @param bool $skipHandling indicates whether the pages search operation is
  1112. * completed
  1113. * @return int|false Integer or false if the search could not be initialized
  1114. * @throws ServerNotAvailableException
  1115. */
  1116. private function count(
  1117. string $filter,
  1118. array $bases,
  1119. ?array $attr = null,
  1120. int $limit = 0,
  1121. int $offset = 0,
  1122. bool $skipHandling = false
  1123. ) {
  1124. $this->logger->debug('Count filter: {filter}', [
  1125. 'app' => 'user_ldap',
  1126. 'filter' => $filter
  1127. ]);
  1128. $limitPerPage = (int)$this->connection->ldapPagingSize;
  1129. if ($limit < $limitPerPage && $limit > 0) {
  1130. $limitPerPage = $limit;
  1131. }
  1132. $counter = 0;
  1133. $count = null;
  1134. $this->connection->getConnectionResource();
  1135. foreach ($bases as $base) {
  1136. do {
  1137. $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
  1138. if ($search === false) {
  1139. return $counter > 0 ? $counter : false;
  1140. }
  1141. [$sr, $pagedSearchOK] = $search;
  1142. /* ++ Fixing RHDS searches with pages with zero results ++
  1143. * countEntriesInSearchResults() method signature changed
  1144. * by removing $limit and &$hasHitLimit parameters
  1145. */
  1146. $count = $this->countEntriesInSearchResults($sr);
  1147. $counter += $count;
  1148. $hasMorePages = $this->processPagedSearchStatus($sr, $count, $limitPerPage, $pagedSearchOK, $skipHandling);
  1149. $offset += $limitPerPage;
  1150. /* ++ Fixing RHDS searches with pages with zero results ++
  1151. * Continue now depends on $hasMorePages value
  1152. */
  1153. $continue = $pagedSearchOK && $hasMorePages;
  1154. } while ($continue && ($limit <= 0 || $limit > $counter));
  1155. }
  1156. return $counter;
  1157. }
  1158. /**
  1159. * @param resource|\LDAP\Result|resource[]|\LDAP\Result[] $sr
  1160. * @return int
  1161. * @throws ServerNotAvailableException
  1162. */
  1163. private function countEntriesInSearchResults($sr): int {
  1164. return (int)$this->invokeLDAPMethod('countEntries', $sr);
  1165. }
  1166. /**
  1167. * Executes an LDAP search
  1168. *
  1169. * @throws ServerNotAvailableException
  1170. */
  1171. public function search(
  1172. string $filter,
  1173. string $base,
  1174. ?array $attr = null,
  1175. ?int $limit = null,
  1176. ?int $offset = null,
  1177. bool $skipHandling = false
  1178. ): array {
  1179. $limitPerPage = (int)$this->connection->ldapPagingSize;
  1180. if (!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
  1181. $limitPerPage = $limit;
  1182. }
  1183. /* ++ Fixing RHDS searches with pages with zero results ++
  1184. * As we can have pages with zero results and/or pages with less
  1185. * than $limit results but with a still valid server 'cookie',
  1186. * loops through until we get $continue equals true and
  1187. * $findings['count'] < $limit
  1188. */
  1189. $findings = [];
  1190. $offset = $offset ?? 0;
  1191. $savedoffset = $offset;
  1192. $iFoundItems = 0;
  1193. do {
  1194. $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
  1195. if ($search === false) {
  1196. return [];
  1197. }
  1198. [$sr, $pagedSearchOK] = $search;
  1199. if ($skipHandling) {
  1200. //i.e. result do not need to be fetched, we just need the cookie
  1201. //thus pass 1 or any other value as $iFoundItems because it is not
  1202. //used
  1203. $this->processPagedSearchStatus($sr, 1, $limitPerPage, $pagedSearchOK, $skipHandling);
  1204. return [];
  1205. }
  1206. $findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $sr));
  1207. $iFoundItems = max($iFoundItems, $findings['count']);
  1208. unset($findings['count']);
  1209. $continue = $this->processPagedSearchStatus($sr, $iFoundItems, $limitPerPage, $pagedSearchOK, $skipHandling);
  1210. $offset += $limitPerPage;
  1211. } while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit));
  1212. // resetting offset
  1213. $offset = $savedoffset;
  1214. if (!is_null($attr)) {
  1215. $selection = [];
  1216. $i = 0;
  1217. foreach ($findings as $item) {
  1218. if (!is_array($item)) {
  1219. continue;
  1220. }
  1221. $item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
  1222. foreach ($attr as $key) {
  1223. if (isset($item[$key])) {
  1224. if (is_array($item[$key]) && isset($item[$key]['count'])) {
  1225. unset($item[$key]['count']);
  1226. }
  1227. if ($key !== 'dn') {
  1228. if ($this->resemblesDN($key)) {
  1229. $selection[$i][$key] = $this->helper->sanitizeDN($item[$key]);
  1230. } elseif ($key === 'objectguid' || $key === 'guid') {
  1231. $selection[$i][$key] = [$this->convertObjectGUID2Str($item[$key][0])];
  1232. } else {
  1233. $selection[$i][$key] = $item[$key];
  1234. }
  1235. } else {
  1236. $selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
  1237. }
  1238. }
  1239. }
  1240. $i++;
  1241. }
  1242. $findings = $selection;
  1243. }
  1244. //we slice the findings, when
  1245. //a) paged search unsuccessful, though attempted
  1246. //b) no paged search, but limit set
  1247. if ((!$this->getPagedSearchResultState()
  1248. && $pagedSearchOK)
  1249. || (
  1250. !$pagedSearchOK
  1251. && !is_null($limit)
  1252. )
  1253. ) {
  1254. $findings = array_slice($findings, $offset, $limit);
  1255. }
  1256. return $findings;
  1257. }
  1258. /**
  1259. * @param string $name
  1260. * @return string
  1261. * @throws \InvalidArgumentException
  1262. */
  1263. public function sanitizeUsername($name) {
  1264. $name = trim($name);
  1265. if ($this->connection->ldapIgnoreNamingRules) {
  1266. return $name;
  1267. }
  1268. // Use htmlentities to get rid of accents
  1269. $name = htmlentities($name, ENT_NOQUOTES, 'UTF-8');
  1270. // Remove accents
  1271. $name = preg_replace('#&([A-Za-z])(?:acute|cedil|caron|circ|grave|orn|ring|slash|th|tilde|uml);#', '\1', $name);
  1272. // Remove ligatures
  1273. $name = preg_replace('#&([A-Za-z]{2})(?:lig);#', '\1', $name);
  1274. // Remove unknown leftover entities
  1275. $name = preg_replace('#&[^;]+;#', '', $name);
  1276. // Replacements
  1277. $name = str_replace(' ', '_', $name);
  1278. // Every remaining disallowed characters will be removed
  1279. $name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
  1280. if (strlen($name) > 64) {
  1281. $name = hash('sha256', $name, false);
  1282. }
  1283. if ($name === '') {
  1284. throw new \InvalidArgumentException('provided name template for username does not contain any allowed characters');
  1285. }
  1286. return $name;
  1287. }
  1288. public function sanitizeGroupIDCandidate(string $candidate): string {
  1289. $candidate = trim($candidate);
  1290. if (strlen($candidate) > 64) {
  1291. $candidate = hash('sha256', $candidate, false);
  1292. }
  1293. if ($candidate === '') {
  1294. throw new \InvalidArgumentException('provided name template for username does not contain any allowed characters');
  1295. }
  1296. return $candidate;
  1297. }
  1298. /**
  1299. * escapes (user provided) parts for LDAP filter
  1300. *
  1301. * @param string $input , the provided value
  1302. * @param bool $allowAsterisk whether in * at the beginning should be preserved
  1303. * @return string the escaped string
  1304. */
  1305. public function escapeFilterPart($input, $allowAsterisk = false): string {
  1306. $asterisk = '';
  1307. if ($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
  1308. $asterisk = '*';
  1309. $input = mb_substr($input, 1, null, 'UTF-8');
  1310. }
  1311. return $asterisk . ldap_escape($input, '', LDAP_ESCAPE_FILTER);
  1312. }
  1313. /**
  1314. * combines the input filters with AND
  1315. *
  1316. * @param string[] $filters the filters to connect
  1317. * @return string the combined filter
  1318. */
  1319. public function combineFilterWithAnd($filters): string {
  1320. return $this->combineFilter($filters, '&');
  1321. }
  1322. /**
  1323. * combines the input filters with OR
  1324. *
  1325. * @param string[] $filters the filters to connect
  1326. * @return string the combined filter
  1327. * Combines Filter arguments with OR
  1328. */
  1329. public function combineFilterWithOr($filters) {
  1330. return $this->combineFilter($filters, '|');
  1331. }
  1332. /**
  1333. * combines the input filters with given operator
  1334. *
  1335. * @param string[] $filters the filters to connect
  1336. * @param string $operator either & or |
  1337. * @return string the combined filter
  1338. */
  1339. private function combineFilter(array $filters, string $operator): string {
  1340. $combinedFilter = '(' . $operator;
  1341. foreach ($filters as $filter) {
  1342. if ($filter !== '' && $filter[0] !== '(') {
  1343. $filter = '(' . $filter . ')';
  1344. }
  1345. $combinedFilter .= $filter;
  1346. }
  1347. $combinedFilter .= ')';
  1348. return $combinedFilter;
  1349. }
  1350. /**
  1351. * creates a filter part for to perform search for users
  1352. *
  1353. * @param string $search the search term
  1354. * @return string the final filter part to use in LDAP searches
  1355. */
  1356. public function getFilterPartForUserSearch($search): string {
  1357. return $this->getFilterPartForSearch($search,
  1358. $this->connection->ldapAttributesForUserSearch,
  1359. $this->connection->ldapUserDisplayName);
  1360. }
  1361. /**
  1362. * creates a filter part for to perform search for groups
  1363. *
  1364. * @param string $search the search term
  1365. * @return string the final filter part to use in LDAP searches
  1366. */
  1367. public function getFilterPartForGroupSearch($search): string {
  1368. return $this->getFilterPartForSearch($search,
  1369. $this->connection->ldapAttributesForGroupSearch,
  1370. $this->connection->ldapGroupDisplayName);
  1371. }
  1372. /**
  1373. * creates a filter part for searches by splitting up the given search
  1374. * string into single words
  1375. *
  1376. * @param string $search the search term
  1377. * @param string[]|null|'' $searchAttributes needs to have at least two attributes,
  1378. * otherwise it does not make sense :)
  1379. * @return string the final filter part to use in LDAP searches
  1380. * @throws DomainException
  1381. */
  1382. private function getAdvancedFilterPartForSearch(string $search, $searchAttributes): string {
  1383. if (!is_array($searchAttributes) || count($searchAttributes) < 2) {
  1384. throw new DomainException('searchAttributes must be an array with at least two string');
  1385. }
  1386. $searchWords = explode(' ', trim($search));
  1387. $wordFilters = [];
  1388. foreach ($searchWords as $word) {
  1389. $word = $this->prepareSearchTerm($word);
  1390. //every word needs to appear at least once
  1391. $wordMatchOneAttrFilters = [];
  1392. foreach ($searchAttributes as $attr) {
  1393. $wordMatchOneAttrFilters[] = $attr . '=' . $word;
  1394. }
  1395. $wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
  1396. }
  1397. return $this->combineFilterWithAnd($wordFilters);
  1398. }
  1399. /**
  1400. * creates a filter part for searches
  1401. *
  1402. * @param string $search the search term
  1403. * @param string[]|null|'' $searchAttributes
  1404. * @param string $fallbackAttribute a fallback attribute in case the user
  1405. * did not define search attributes. Typically the display name attribute.
  1406. * @return string the final filter part to use in LDAP searches
  1407. */
  1408. private function getFilterPartForSearch(string $search, $searchAttributes, string $fallbackAttribute): string {
  1409. $filter = [];
  1410. $haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
  1411. if ($haveMultiSearchAttributes && str_contains(trim($search), ' ')) {
  1412. try {
  1413. return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
  1414. } catch (DomainException $e) {
  1415. // Creating advanced filter for search failed, falling back to simple method. Edge case, but valid.
  1416. }
  1417. }
  1418. $originalSearch = $search;
  1419. $search = $this->prepareSearchTerm($search);
  1420. if (!is_array($searchAttributes) || count($searchAttributes) === 0) {
  1421. if ($fallbackAttribute === '') {
  1422. return '';
  1423. }
  1424. // wildcards don't work with some attributes
  1425. if ($originalSearch !== '') {
  1426. $filter[] = $fallbackAttribute . '=' . $originalSearch;
  1427. }
  1428. $filter[] = $fallbackAttribute . '=' . $search;
  1429. } else {
  1430. foreach ($searchAttributes as $attribute) {
  1431. // wildcards don't work with some attributes
  1432. if ($originalSearch !== '') {
  1433. $filter[] = $attribute . '=' . $originalSearch;
  1434. }
  1435. $filter[] = $attribute . '=' . $search;
  1436. }
  1437. }
  1438. if (count($filter) === 1) {
  1439. return '(' . $filter[0] . ')';
  1440. }
  1441. return $this->combineFilterWithOr($filter);
  1442. }
  1443. /**
  1444. * returns the search term depending on whether we are allowed
  1445. * list users found by ldap with the current input appended by
  1446. * a *
  1447. */
  1448. private function prepareSearchTerm(string $term): string {
  1449. $config = \OC::$server->getConfig();
  1450. $allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
  1451. $result = $term;
  1452. if ($term === '') {
  1453. $result = '*';
  1454. } elseif ($allowEnum !== 'no') {
  1455. $result = $term . '*';
  1456. }
  1457. return $result;
  1458. }
  1459. /**
  1460. * returns the filter used for counting users
  1461. */
  1462. public function getFilterForUserCount(): string {
  1463. $filter = $this->combineFilterWithAnd([
  1464. $this->connection->ldapUserFilter,
  1465. $this->connection->ldapUserDisplayName . '=*'
  1466. ]);
  1467. return $filter;
  1468. }
  1469. public function areCredentialsValid(string $name, string $password): bool {
  1470. if ($name === '' || $password === '') {
  1471. return false;
  1472. }
  1473. $name = $this->helper->DNasBaseParameter($name);
  1474. $testConnection = clone $this->connection;
  1475. $credentials = [
  1476. 'ldapAgentName' => $name,
  1477. 'ldapAgentPassword' => $password,
  1478. ];
  1479. if (!$testConnection->setConfiguration($credentials)) {
  1480. return false;
  1481. }
  1482. return $testConnection->bind();
  1483. }
  1484. /**
  1485. * reverse lookup of a DN given a known UUID
  1486. *
  1487. * @param string $uuid
  1488. * @return string
  1489. * @throws \Exception
  1490. */
  1491. public function getUserDnByUuid($uuid) {
  1492. $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
  1493. $filter = $this->connection->ldapUserFilter;
  1494. $bases = $this->connection->ldapBaseUsers;
  1495. if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
  1496. // Sacrebleu! The UUID attribute is unknown :( We need first an
  1497. // existing DN to be able to reliably detect it.
  1498. foreach ($bases as $base) {
  1499. $result = $this->search($filter, $base, ['dn'], 1);
  1500. if (!isset($result[0]) || !isset($result[0]['dn'])) {
  1501. continue;
  1502. }
  1503. $dn = $result[0]['dn'][0];
  1504. if ($hasFound = $this->detectUuidAttribute($dn, true)) {
  1505. break;
  1506. }
  1507. }
  1508. if (!isset($hasFound) || !$hasFound) {
  1509. throw new \Exception('Cannot determine UUID attribute');
  1510. }
  1511. } else {
  1512. // The UUID attribute is either known or an override is given.
  1513. // By calling this method we ensure that $this->connection->$uuidAttr
  1514. // is definitely set
  1515. if (!$this->detectUuidAttribute('', true)) {
  1516. throw new \Exception('Cannot determine UUID attribute');
  1517. }
  1518. }
  1519. $uuidAttr = $this->connection->ldapUuidUserAttribute;
  1520. if ($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
  1521. $uuid = $this->formatGuid2ForFilterUser($uuid);
  1522. }
  1523. $filter = $uuidAttr . '=' . $uuid;
  1524. $result = $this->searchUsers($filter, ['dn'], 2);
  1525. if (isset($result[0]['dn']) && count($result) === 1) {
  1526. // we put the count into account to make sure that this is
  1527. // really unique
  1528. return $result[0]['dn'][0];
  1529. }
  1530. throw new \Exception('Cannot determine UUID attribute');
  1531. }
  1532. /**
  1533. * auto-detects the directory's UUID attribute
  1534. *
  1535. * @param string $dn a known DN used to check against
  1536. * @param bool $isUser
  1537. * @param bool $force the detection should be run, even if it is not set to auto
  1538. * @param array|null $ldapRecord
  1539. * @return bool true on success, false otherwise
  1540. * @throws ServerNotAvailableException
  1541. */
  1542. private function detectUuidAttribute(string $dn, bool $isUser = true, bool $force = false, ?array $ldapRecord = null): bool {
  1543. if ($isUser) {
  1544. $uuidAttr = 'ldapUuidUserAttribute';
  1545. $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
  1546. } else {
  1547. $uuidAttr = 'ldapUuidGroupAttribute';
  1548. $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
  1549. }
  1550. if (!$force) {
  1551. if ($this->connection->$uuidAttr !== 'auto') {
  1552. return true;
  1553. } elseif (is_string($uuidOverride) && trim($uuidOverride) !== '') {
  1554. $this->connection->$uuidAttr = $uuidOverride;
  1555. return true;
  1556. }
  1557. $attribute = $this->connection->getFromCache($uuidAttr);
  1558. if ($attribute !== null) {
  1559. $this->connection->$uuidAttr = $attribute;
  1560. return true;
  1561. }
  1562. }
  1563. foreach (self::UUID_ATTRIBUTES as $attribute) {
  1564. if ($ldapRecord !== null) {
  1565. // we have the info from LDAP already, we don't need to talk to the server again
  1566. if (isset($ldapRecord[$attribute])) {
  1567. $this->connection->$uuidAttr = $attribute;
  1568. return true;
  1569. }
  1570. }
  1571. $value = $this->readAttribute($dn, $attribute);
  1572. if (is_array($value) && isset($value[0]) && !empty($value[0])) {
  1573. $this->logger->debug(
  1574. 'Setting {attribute} as {subject}',
  1575. [
  1576. 'app' => 'user_ldap',
  1577. 'attribute' => $attribute,
  1578. 'subject' => $uuidAttr
  1579. ]
  1580. );
  1581. $this->connection->$uuidAttr = $attribute;
  1582. $this->connection->writeToCache($uuidAttr, $attribute);
  1583. return true;
  1584. }
  1585. }
  1586. $this->logger->debug('Could not autodetect the UUID attribute', ['app' => 'user_ldap']);
  1587. return false;
  1588. }
  1589. /**
  1590. * @param array|null $ldapRecord
  1591. * @return false|string
  1592. * @throws ServerNotAvailableException
  1593. */
  1594. public function getUUID(string $dn, bool $isUser = true, ?array $ldapRecord = null) {
  1595. if ($isUser) {
  1596. $uuidAttr = 'ldapUuidUserAttribute';
  1597. $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
  1598. } else {
  1599. $uuidAttr = 'ldapUuidGroupAttribute';
  1600. $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
  1601. }
  1602. $uuid = false;
  1603. if ($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) {
  1604. $attr = $this->connection->$uuidAttr;
  1605. $uuid = $ldapRecord[$attr] ?? $this->readAttribute($dn, $attr);
  1606. if (!is_array($uuid)
  1607. && $uuidOverride !== ''
  1608. && $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord)) {
  1609. $uuid = isset($ldapRecord[$this->connection->$uuidAttr])
  1610. ? $ldapRecord[$this->connection->$uuidAttr]
  1611. : $this->readAttribute($dn, $this->connection->$uuidAttr);
  1612. }
  1613. if (is_array($uuid) && !empty($uuid[0])) {
  1614. $uuid = $uuid[0];
  1615. }
  1616. }
  1617. return $uuid;
  1618. }
  1619. /**
  1620. * converts a binary ObjectGUID into a string representation
  1621. *
  1622. * @param string $oguid the ObjectGUID in its binary form as retrieved from AD
  1623. * @link https://www.php.net/manual/en/function.ldap-get-values-len.php#73198
  1624. */
  1625. private function convertObjectGUID2Str(string $oguid): string {
  1626. $hex_guid = bin2hex($oguid);
  1627. $hex_guid_to_guid_str = '';
  1628. for ($k = 1; $k <= 4; ++$k) {
  1629. $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
  1630. }
  1631. $hex_guid_to_guid_str .= '-';
  1632. for ($k = 1; $k <= 2; ++$k) {
  1633. $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
  1634. }
  1635. $hex_guid_to_guid_str .= '-';
  1636. for ($k = 1; $k <= 2; ++$k) {
  1637. $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
  1638. }
  1639. $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
  1640. $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
  1641. return strtoupper($hex_guid_to_guid_str);
  1642. }
  1643. /**
  1644. * the first three blocks of the string-converted GUID happen to be in
  1645. * reverse order. In order to use it in a filter, this needs to be
  1646. * corrected. Furthermore the dashes need to be replaced and \\ prepended
  1647. * to every two hex figures.
  1648. *
  1649. * If an invalid string is passed, it will be returned without change.
  1650. */
  1651. public function formatGuid2ForFilterUser(string $guid): string {
  1652. $blocks = explode('-', $guid);
  1653. if (count($blocks) !== 5) {
  1654. /*
  1655. * Why not throw an Exception instead? This method is a utility
  1656. * called only when trying to figure out whether a "missing" known
  1657. * LDAP user was or was not renamed on the LDAP server. And this
  1658. * even on the use case that a reverse lookup is needed (UUID known,
  1659. * not DN), i.e. when finding users (search dialog, users page,
  1660. * login, …) this will not be fired. This occurs only if shares from
  1661. * a users are supposed to be mounted who cannot be found. Throwing
  1662. * an exception here would kill the experience for a valid, acting
  1663. * user. Instead we write a log message.
  1664. */
  1665. $this->logger->info(
  1666. 'Passed string does not resemble a valid GUID. Known UUID ' .
  1667. '({uuid}) probably does not match UUID configuration.',
  1668. ['app' => 'user_ldap', 'uuid' => $guid]
  1669. );
  1670. return $guid;
  1671. }
  1672. for ($i = 0; $i < 3; $i++) {
  1673. $pairs = str_split($blocks[$i], 2);
  1674. $pairs = array_reverse($pairs);
  1675. $blocks[$i] = implode('', $pairs);
  1676. }
  1677. for ($i = 0; $i < 5; $i++) {
  1678. $pairs = str_split($blocks[$i], 2);
  1679. $blocks[$i] = '\\' . implode('\\', $pairs);
  1680. }
  1681. return implode('', $blocks);
  1682. }
  1683. /**
  1684. * gets a SID of the domain of the given dn
  1685. *
  1686. * @param string $dn
  1687. * @return string|bool
  1688. * @throws ServerNotAvailableException
  1689. */
  1690. public function getSID($dn) {
  1691. $domainDN = $this->getDomainDNFromDN($dn);
  1692. $cacheKey = 'getSID-' . $domainDN;
  1693. $sid = $this->connection->getFromCache($cacheKey);
  1694. if (!is_null($sid)) {
  1695. return $sid;
  1696. }
  1697. $objectSid = $this->readAttribute($domainDN, 'objectsid');
  1698. if (!is_array($objectSid) || empty($objectSid)) {
  1699. $this->connection->writeToCache($cacheKey, false);
  1700. return false;
  1701. }
  1702. $domainObjectSid = $this->convertSID2Str($objectSid[0]);
  1703. $this->connection->writeToCache($cacheKey, $domainObjectSid);
  1704. return $domainObjectSid;
  1705. }
  1706. /**
  1707. * converts a binary SID into a string representation
  1708. *
  1709. * @param string $sid
  1710. * @return string
  1711. */
  1712. public function convertSID2Str($sid) {
  1713. // The format of a SID binary string is as follows:
  1714. // 1 byte for the revision level
  1715. // 1 byte for the number n of variable sub-ids
  1716. // 6 bytes for identifier authority value
  1717. // n*4 bytes for n sub-ids
  1718. //
  1719. // Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
  1720. // Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
  1721. $revision = ord($sid[0]);
  1722. $numberSubID = ord($sid[1]);
  1723. $subIdStart = 8; // 1 + 1 + 6
  1724. $subIdLength = 4;
  1725. if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
  1726. // Incorrect number of bytes present.
  1727. return '';
  1728. }
  1729. // 6 bytes = 48 bits can be represented using floats without loss of
  1730. // precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
  1731. $iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
  1732. $subIDs = [];
  1733. for ($i = 0; $i < $numberSubID; $i++) {
  1734. $subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
  1735. $subIDs[] = sprintf('%u', $subID[1]);
  1736. }
  1737. // Result for example above: S-1-5-21-249921958-728525901-1594176202
  1738. return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
  1739. }
  1740. /**
  1741. * checks if the given DN is part of the given base DN(s)
  1742. *
  1743. * @param string[] $bases array containing the allowed base DN or DNs
  1744. */
  1745. public function isDNPartOfBase(string $dn, array $bases): bool {
  1746. $belongsToBase = false;
  1747. $bases = $this->helper->sanitizeDN($bases);
  1748. foreach ($bases as $base) {
  1749. $belongsToBase = true;
  1750. if (mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8') - mb_strlen($base, 'UTF-8'))) {
  1751. $belongsToBase = false;
  1752. }
  1753. if ($belongsToBase) {
  1754. break;
  1755. }
  1756. }
  1757. return $belongsToBase;
  1758. }
  1759. /**
  1760. * resets a running Paged Search operation
  1761. *
  1762. * @throws ServerNotAvailableException
  1763. */
  1764. private function abandonPagedSearch(): void {
  1765. if ($this->lastCookie === '') {
  1766. return;
  1767. }
  1768. $this->getPagedSearchResultState();
  1769. $this->lastCookie = '';
  1770. }
  1771. /**
  1772. * checks whether an LDAP paged search operation has more pages that can be
  1773. * retrieved, typically when offset and limit are provided.
  1774. *
  1775. * Be very careful to use it: the last cookie value, which is inspected, can
  1776. * be reset by other operations. Best, call it immediately after a search(),
  1777. * searchUsers() or searchGroups() call. count-methods are probably safe as
  1778. * well. Don't rely on it with any fetchList-method.
  1779. *
  1780. * @return bool
  1781. */
  1782. public function hasMoreResults() {
  1783. if ($this->lastCookie === '') {
  1784. // as in RFC 2696, when all results are returned, the cookie will
  1785. // be empty.
  1786. return false;
  1787. }
  1788. return true;
  1789. }
  1790. /**
  1791. * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
  1792. *
  1793. * @return boolean|null true on success, null or false otherwise
  1794. */
  1795. public function getPagedSearchResultState() {
  1796. $result = $this->pagedSearchedSuccessful;
  1797. $this->pagedSearchedSuccessful = null;
  1798. return $result;
  1799. }
  1800. /**
  1801. * Prepares a paged search, if possible
  1802. *
  1803. * @param string $filter the LDAP filter for the search
  1804. * @param string $base the LDAP subtree that shall be searched
  1805. * @param string[] $attr optional, when a certain attribute shall be filtered outside
  1806. * @param int $limit
  1807. * @param int $offset
  1808. * @return array{bool, int, string}
  1809. * @throws ServerNotAvailableException
  1810. * @throws NoMoreResults
  1811. */
  1812. private function initPagedSearch(
  1813. string $filter,
  1814. string $base,
  1815. ?array $attr,
  1816. int $pageSize,
  1817. int $offset
  1818. ): array {
  1819. $pagedSearchOK = false;
  1820. if ($pageSize !== 0) {
  1821. $this->logger->debug(
  1822. 'initializing paged search for filter {filter}, base {base}, attr {attr}, pageSize {pageSize}, offset {offset}',
  1823. [
  1824. 'app' => 'user_ldap',
  1825. 'filter' => $filter,
  1826. 'base' => $base,
  1827. 'attr' => $attr,
  1828. 'pageSize' => $pageSize,
  1829. 'offset' => $offset
  1830. ]
  1831. );
  1832. // Get the cookie from the search for the previous search, required by LDAP
  1833. if (($this->lastCookie === '') && ($offset > 0)) {
  1834. // no cookie known from a potential previous search. We need
  1835. // to start from 0 to come to the desired page. cookie value
  1836. // of '0' is valid, because 389ds
  1837. $defaultPageSize = (int)$this->connection->ldapPagingSize;
  1838. if ($offset < $defaultPageSize) {
  1839. /* Make a search with offset as page size and dismiss the result, to init the cookie */
  1840. $this->search($filter, $base, $attr, $offset, 0, true);
  1841. } else {
  1842. /* Make a search for previous page and dismiss the result, to init the cookie */
  1843. $reOffset = $offset - $defaultPageSize;
  1844. $this->search($filter, $base, $attr, $defaultPageSize, $reOffset, true);
  1845. }
  1846. if (!$this->hasMoreResults()) {
  1847. // when the cookie is reset with != 0 offset, there are no further
  1848. // results, so stop.
  1849. throw new NoMoreResults();
  1850. }
  1851. }
  1852. if ($this->lastCookie !== '' && $offset === 0) {
  1853. //since offset = 0, this is a new search. We abandon other searches that might be ongoing.
  1854. $this->abandonPagedSearch();
  1855. }
  1856. $this->logger->debug('Ready for a paged search', ['app' => 'user_ldap']);
  1857. return [true, $pageSize, $this->lastCookie];
  1858. /* ++ Fixing RHDS searches with pages with zero results ++
  1859. * We couldn't get paged searches working with our RHDS for login ($limit = 0),
  1860. * due to pages with zero results.
  1861. * So we added "&& !empty($this->lastCookie)" to this test to ignore pagination
  1862. * if we don't have a previous paged search.
  1863. */
  1864. } elseif ($this->lastCookie !== '') {
  1865. // a search without limit was requested. However, if we do use
  1866. // Paged Search once, we always must do it. This requires us to
  1867. // initialize it with the configured page size.
  1868. $this->abandonPagedSearch();
  1869. // in case someone set it to 0 … use 500, otherwise no results will
  1870. // be returned.
  1871. $pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500;
  1872. return [true, $pageSize, $this->lastCookie];
  1873. }
  1874. return [false, $pageSize, ''];
  1875. }
  1876. /**
  1877. * Is more than one $attr used for search?
  1878. *
  1879. * @param string|string[]|null $attr
  1880. * @return bool
  1881. */
  1882. private function manyAttributes($attr): bool {
  1883. if (\is_array($attr)) {
  1884. return \count($attr) > 1;
  1885. }
  1886. return false;
  1887. }
  1888. }