BxDolAcl.php 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defgroup UnaCore UNA Core
  7. * @{
  8. */
  9. /**
  10. * Message constants passed to _t_ext() function by checkAction()
  11. *
  12. * NOTE: checkAction() returns language dependent messages
  13. */
  14. define('CHECK_ACTION_MESSAGE_NOT_ALLOWED', "_sys_acl_action_not_allowed");
  15. define('CHECK_ACTION_MESSAGE_LIMIT_REACHED', "_sys_acl_action_limit_reached");
  16. define('CHECK_ACTION_MESSAGE_MESSAGE_EVERY_PERIOD', "_sys_acl_action_every_period");
  17. define('CHECK_ACTION_MESSAGE_NOT_ALLOWED_BEFORE', "_sys_acl_action_not_allowed_before");
  18. define('CHECK_ACTION_MESSAGE_NOT_ALLOWED_AFTER', "_sys_acl_action_not_allowed_after");
  19. define('CHECK_ACTION_MESSAGE_UNAUTHENTICATED', "_sys_acl_action_unauthenticated");
  20. define('CHECK_ACTION_MESSAGE_UNCONFIRMED', "_sys_acl_action_unconfirmed");
  21. define('CHECK_ACTION_MESSAGE_PENDING', "_sys_acl_action_pending");
  22. define('CHECK_ACTION_MESSAGE_SUSPENDED', "_sys_acl_action_suspended");
  23. /**
  24. * Nodes of $args array that are passed to _t_ext() function by checkAction()
  25. */
  26. define('CHECK_ACTION_LANG_FILE_ACTION', 1);
  27. define('CHECK_ACTION_LANG_FILE_MEMBERSHIP', 2);
  28. define('CHECK_ACTION_LANG_FILE_LIMIT', 3);
  29. define('CHECK_ACTION_LANG_FILE_PERIOD', 4);
  30. define('CHECK_ACTION_LANG_FILE_AFTER', 5);
  31. define('CHECK_ACTION_LANG_FILE_BEFORE', 6);
  32. define('CHECK_ACTION_LANG_FILE_SITE_EMAIL', 7);
  33. define('CHECK_ACTION_LANG_FILE_PERIOD_RESTART_AT', 8);
  34. /**
  35. * Standard membership ID's
  36. */
  37. define('MEMBERSHIP_ID_NON_MEMBER', 1);
  38. define('MEMBERSHIP_ID_ACCOUNT', 2);
  39. define('MEMBERSHIP_ID_STANDARD', 3);
  40. define('MEMBERSHIP_ID_UNCONFIRMED', 4);
  41. define('MEMBERSHIP_ID_PENDING', 5);
  42. define('MEMBERSHIP_ID_SUSPENDED', 6);
  43. define('MEMBERSHIP_ID_MODERATOR', 7);
  44. define('MEMBERSHIP_ID_ADMINISTRATOR', 8);
  45. /**
  46. * Standard membership actions
  47. */
  48. define('MEMBERSHIP_ACTION_EDIT_ANY_ENTRY', 'edit any entry');
  49. define('MEMBERSHIP_ACTION_DELETE_ANY_ENTRY', 'delete any entry');
  50. /**
  51. * Indices for checkAction() result array
  52. */
  53. define('CHECK_ACTION_RESULT', 0);
  54. define('CHECK_ACTION_MESSAGE', 1);
  55. define('CHECK_ACTION_PARAMETER', 3);
  56. /**
  57. * CHECK_ACTION_RESULT node values
  58. */
  59. define('CHECK_ACTION_RESULT_ALLOWED', 0);
  60. define('CHECK_ACTION_RESULT_NOT_ALLOWED', 1);
  61. define('CHECK_ACTION_RESULT_NOT_ACTIVE', 2);
  62. define('CHECK_ACTION_RESULT_LIMIT_REACHED', 3);
  63. define('CHECK_ACTION_RESULT_NOT_ALLOWED_BEFORE', 4);
  64. define('CHECK_ACTION_RESULT_NOT_ALLOWED_AFTER', 5);
  65. /**
  66. * Standard period units
  67. */
  68. define('MEMBERSHIP_PERIOD_UNIT_DAY', 'day');
  69. define('MEMBERSHIP_PERIOD_UNIT_WEEK', 'week');
  70. define('MEMBERSHIP_PERIOD_UNIT_MONTH', 'month');
  71. define('MEMBERSHIP_PERIOD_UNIT_YEAR', 'year');
  72. class BxDolAcl extends BxDolFactory implements iBxDolSingleton
  73. {
  74. protected static $_aCacheData = array();
  75. protected $oDb;
  76. protected $_aStandardMemberships = array(
  77. MEMBERSHIP_ID_NON_MEMBER => 1,
  78. MEMBERSHIP_ID_ACCOUNT => 1,
  79. MEMBERSHIP_ID_UNCONFIRMED => 1,
  80. MEMBERSHIP_ID_PENDING => 1,
  81. MEMBERSHIP_ID_SUSPENDED => 1,
  82. MEMBERSHIP_ID_STANDARD => 1,
  83. );
  84. protected $_aProfileStatus2LevelMap = array (
  85. BX_PROFILE_STATUS_SUSPENDED => MEMBERSHIP_ID_SUSPENDED,
  86. BX_PROFILE_STATUS_PENDING => MEMBERSHIP_ID_PENDING,
  87. );
  88. protected $_aLevel2MessageMap = array (
  89. MEMBERSHIP_ID_NON_MEMBER => '_sys_acl_action_unauthenticated',
  90. MEMBERSHIP_ID_ACCOUNT => '_sys_acl_action_account',
  91. MEMBERSHIP_ID_UNCONFIRMED => '_sys_acl_action_unconfirmed',
  92. MEMBERSHIP_ID_PENDING => '_sys_acl_action_pending',
  93. MEMBERSHIP_ID_SUSPENDED => '_sys_acl_action_suspended',
  94. );
  95. protected function __construct()
  96. {
  97. if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
  98. trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
  99. parent::__construct();
  100. $this->oDb = BxDolAclQuery::getInstance();
  101. }
  102. /**
  103. * Prevent cloning the instance
  104. */
  105. public function __clone()
  106. {
  107. if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
  108. trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
  109. }
  110. /**
  111. * Get singleton instance of the class
  112. */
  113. public static function getInstance()
  114. {
  115. if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
  116. $GLOBALS['bxDolClasses'][__CLASS__] = new BxTemplAcl();
  117. return $GLOBALS['bxDolClasses'][__CLASS__];
  118. }
  119. /**
  120. * Get necessary condition array to use membership level in search classes
  121. * @param $sContentField content table field name
  122. * @param $mixedLevelId level ID or array of level IDs
  123. * @return array of conditions is returned
  124. */
  125. public function getContentByLevelAsCondition($sContentField, $mixedLevelId)
  126. {
  127. $iLevelId = !is_array($mixedLevelId) ? $mixedLevelId : 0;
  128. if (!$iLevelId && is_array($mixedLevelId) && 1 == count($mixedLevelId)) {
  129. $a = array_values($mixedLevelId);
  130. $iLevelId = array_shift($a);
  131. }
  132. // unconfirmed
  133. if (MEMBERSHIP_ID_UNCONFIRMED == $iLevelId) {
  134. return array(
  135. 'restriction_sql' => ' AND `sys_accounts`.`email_confirmed` = 0 ',
  136. 'restriction' => array (),
  137. 'join' => array (),
  138. );
  139. }
  140. // standard
  141. elseif (MEMBERSHIP_ID_STANDARD == $iLevelId) {
  142. return array(
  143. 'restriction_sql' => ' AND (`tlm`.DateStarts IS NULL OR `tlm`.DateStarts <= NOW()) AND (`tlm`.DateExpires IS NULL OR `tlm`.DateExpires > NOW()) AND `tlm`.`IDMember` IS NULL AND `sys_accounts`.`email_confirmed` != 0 ',
  144. 'restriction' => array (
  145. ),
  146. 'join' => array (
  147. 'acl_members' => array(
  148. 'type' => 'LEFT',
  149. 'table' => 'sys_acl_levels_members',
  150. 'table_alias' => 'tlm',
  151. 'mainField' => $sContentField,
  152. 'onField' => 'IDMember',
  153. 'joinFields' => array(),
  154. ),
  155. ),
  156. );
  157. }
  158. // other levels
  159. else {
  160. return array(
  161. 'restriction_sql' => ' AND (`tlm`.DateStarts IS NULL OR `tlm`.DateStarts <= NOW()) AND (`tlm`.DateExpires IS NULL OR `tlm`.DateExpires > NOW()) AND `sys_accounts`.`email_confirmed` != 0 ',
  162. 'restriction' => array (
  163. 'acl_members' => array(
  164. 'value' => $mixedLevelId,
  165. 'field' => 'IDLevel',
  166. 'operator' => is_array($mixedLevelId) ? 'in' : '=',
  167. 'table' => 'tlm',
  168. ),
  169. ),
  170. 'join' => array (
  171. 'acl_members' => array(
  172. 'type' => 'INNER',
  173. 'table' => 'sys_acl_levels_members',
  174. 'table_alias' => 'tlm',
  175. 'mainField' => $sContentField,
  176. 'onField' => 'IDMember',
  177. 'joinFields' => array(),
  178. ),
  179. ),
  180. );
  181. }
  182. }
  183. /**
  184. * Get necessary parts of SQL query to use membership levels in other queries
  185. * @param $sContentTable content table name
  186. * @param $sContentField content table field name
  187. * @param $mixedLevelId level ID or array of level IDs
  188. * @return array of SQL string parts, for now 'where' part only is returned
  189. */
  190. public function getContentByLevelAsSQLPart($sContentTable, $sContentField, $mixedLevelId)
  191. {
  192. return $this->oDb->getContentByLevelAsSQLPart($sContentTable, $sContentField, $mixedLevelId);
  193. }
  194. /**
  195. * Check if member has one of the provided membership levels
  196. * @param $mixedPermissions - integer value (every bit is matched with some membership ID) or an array of membership IDs to check permissions for
  197. * @param $iProfileId - profile to check, if it isn't provided or is false then currently logged in profile is used.
  198. * @return true if member has privided membership levels, or false if member hasn't.
  199. */
  200. public function isMemberLevelInSet($mixedPermissions, $iProfileId = false)
  201. {
  202. $iPermissions = 0;
  203. if(is_array($mixedPermissions))
  204. foreach($mixedPermissions as $iPermissionId)
  205. $iPermissions += pow(2, (int)$iPermissionId - 1);
  206. else if(is_numeric($mixedPermissions))
  207. $iPermissions = (int)$mixedPermissions;
  208. if(!$iPermissions)
  209. return false;
  210. return ($iPermissions & $this->getMemberLevelBit($iProfileId));
  211. }
  212. /**
  213. * Get user's membership level bit for bitwise operarions
  214. */
  215. public function getMemberLevelBit($iProfileId = 0)
  216. {
  217. if (!$iProfileId)
  218. $iProfileId = bx_get_logged_profile_id();
  219. $aACL = $this->getMemberMembershipInfo($iProfileId);
  220. return pow(2, $aACL['id'] - 1);
  221. }
  222. /**
  223. * Checks if a given action is allowed for a given profile and updates action information if the
  224. * action is performed.
  225. *
  226. * @param int $iProfileId ID of a profile that is going to perform an action
  227. * @param int $iActionId ID of the action itself
  228. * @param boolean $bPerformAction if true, then action information is updated, i.e. action is 'performed'
  229. * @return array(
  230. * CHECK_ACTION_RESULT => CHECK_ACTION_RESULT_ constant,
  231. * CHECK_ACTION_MESSAGE => CHECK_ACTION_MESSAGE_ constant,
  232. * CHECK_ACTION_PARAMETER => additional action parameter (string)
  233. * )
  234. *
  235. * NOTES:
  236. *
  237. * $aResult[CHECK_ACTION_MESSAGE] contains a message with detailed information about the result,
  238. * already processed by the language file
  239. *
  240. * if $aResult[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED then this node contains
  241. * an empty string
  242. *
  243. * The error messages themselves are stored in the language file. Additional variables are
  244. * passed to the languages.inc.php function _t_ext() as an array and can be used there in the form of
  245. * {0}, {1}, {2} ...
  246. *
  247. * Additional variables passed to the lang. file on errors (can be used in error messages):
  248. *
  249. * For all errors:
  250. *
  251. * $arg0[CHECK_ACTION_LANG_FILE_ACTION] = name of the action
  252. * $arg0[CHECK_ACTION_LANG_FILE_MEMBERSHIP]= name of the current membership
  253. *
  254. * CHECK_ACTION_RESULT_LIMIT_REACHED:
  255. *
  256. * $arg0[CHECK_ACTION_LANG_FILE_LIMIT] = limit on number of actions allowed for the profile
  257. * $arg0[CHECK_ACTION_LANG_FILE_PERIOD] = period that the limit is set for (in hours, 0 if unlimited)
  258. * $arg0[CHECK_ACTION_LANG_FILE_PERIOD_RESTART_AT] = time when new period begins, so counter will be reset
  259. *
  260. * CHECK_ACTION_RESULT_NOT_ALLOWED_BEFORE:
  261. *
  262. * $arg0[CHECK_ACTION_LANG_FILE_BEFORE] = date/time since when the action is allowed
  263. *
  264. * CHECK_ACTION_RESULT_NOT_ALLOWED_AFTER:
  265. *
  266. * $arg0[CHECK_ACTION_LANG_FILE_AFTER] = date/time since when the action is not allowed
  267. *
  268. * $aResult[CHECK_ACTION_PARAMETER] contains an additional parameter that can be considered
  269. * when performing the action (like the number of profiles to show in search result)
  270. */
  271. function checkAction($iProfileId, $iActionId, $bPerformAction = false)
  272. {
  273. $aResult = array();
  274. $aLangFileParams = array();
  275. $iProfileId = (int)$iProfileId;
  276. $iActionId = (int)$iActionId;
  277. $bPerformAction = $bPerformAction ? true : false;
  278. $aMembership = $this->getMemberMembershipInfo($iProfileId); // get current profile's membership information
  279. $aLangFileParams[CHECK_ACTION_LANG_FILE_MEMBERSHIP] = _t($aMembership['name']);
  280. $aLangFileParams[CHECK_ACTION_LANG_FILE_SITE_EMAIL] = getParam('site_email');
  281. $aAction = $this->oDb->getAction($aMembership['id'], $iActionId);
  282. if (!$aAction)
  283. bx_trigger_error('Unknown action ID: ' . $iActionId, 2);
  284. $aResult[CHECK_ACTION_PARAMETER] = $aAction['additional_param_value'];
  285. $aLangFileParams[CHECK_ACTION_LANG_FILE_ACTION] = !empty($aAction['title']) ? _t($aAction['title']) : $aAction['name'];
  286. /* Check membership action with ability to overwrite params - begin */
  287. $bContinue = true;
  288. /**
  289. * @hooks
  290. * @hookdef hook-membership-check_action 'membership', 'check_action' - hook to override the result of checking whether an action is allowed or not
  291. * - $unit_name - equals `membership`
  292. * - $action - equals `check_action`
  293. * - $object_id - action id
  294. * - $sender_id - performer profile id
  295. * - $extra_params - array of additional params with the following array keys:
  296. * - `action` - [string] by ref, action name, can be overridden in hook processing
  297. * - `perform` - [boolean] by ref, check only or mark action as performed after checking, can be overridden in hook processing
  298. * - `result` - [array] by ref, check action result array, can be overridden in hook processing
  299. * - `lang` - [array] by ref, array of parsable into check result message variables, can be overridden in hook processing
  300. * - `continue` - [boolean] by ref, continue or not the default check operation after the hook, can be overridden in hook processing
  301. * @hook @ref hook-membership-check_action
  302. */
  303. bx_alert('membership', 'check_action', $iActionId, $iProfileId, array(
  304. 'action' => &$aAction,
  305. 'perform' => &$bPerformAction,
  306. 'result' => &$aResult,
  307. 'lang' => &$aLangFileParams,
  308. 'continue' => &$bContinue
  309. ));
  310. if (!$bContinue)
  311. return $aResult;
  312. /* Check membership action with ability to overwrite params - end */
  313. /**
  314. * Action is not allowed for the current membership
  315. */
  316. if (is_null($aAction['id'])) {
  317. $sLangKey = CHECK_ACTION_MESSAGE_NOT_ALLOWED;
  318. if (isset($this->_aLevel2MessageMap[$aMembership['id']]))
  319. $sLangKey = $this->_aLevel2MessageMap[$aMembership['id']];
  320. $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_NOT_ALLOWED;
  321. $aResult[CHECK_ACTION_MESSAGE] = _t_ext($sLangKey, $aLangFileParams);
  322. return $aResult;
  323. }
  324. /**
  325. * Check fixed period limitations if present (also for non-members)
  326. */
  327. if($aAction['allowed_period_start'] && time() < $aAction['allowed_period_start']) {
  328. $aLangFileParams[CHECK_ACTION_LANG_FILE_BEFORE] = bx_time_js($aAction['allowed_period_start'], BX_FORMAT_DATE_TIME);
  329. $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_NOT_ALLOWED_BEFORE;
  330. $aResult[CHECK_ACTION_MESSAGE] = _t_ext(CHECK_ACTION_MESSAGE_NOT_ALLOWED_BEFORE, $aLangFileParams);
  331. return $aResult;
  332. }
  333. if($aAction['allowed_period_end'] && time() > $aAction['allowed_period_end']) {
  334. $aLangFileParams[CHECK_ACTION_LANG_FILE_AFTER] = bx_time_js($aAction['allowed_period_end'], BX_FORMAT_DATE_TIME);
  335. $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_NOT_ALLOWED_AFTER;
  336. $aResult[CHECK_ACTION_MESSAGE] = _t_ext(CHECK_ACTION_MESSAGE_NOT_ALLOWED_AFTER, $aLangFileParams);
  337. return $aResult;
  338. }
  339. /**
  340. * if non-member, allow action without performing further checks
  341. */
  342. if ($aMembership['id'] == MEMBERSHIP_ID_NON_MEMBER) {
  343. $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_ALLOWED;
  344. return $aResult;
  345. }
  346. /**
  347. * Check other limitations (for members only)
  348. */
  349. $iAllowedCnt = (int)$aAction['allowed_count']; ///< Number of allowed actions. Unlimited if not specified or 0
  350. $iPeriodLen = (int)$aAction['allowed_period_len']; ///< Period for AllowedCount in hours. If not specified, AllowedCount is treated as total number of actions permitted.
  351. if($iAllowedCnt > 0) {
  352. $aActionTrack = $this->oDb->getActionTrack($iActionId, $iProfileId);
  353. $iActionsLeft = $bPerformAction ? $iAllowedCnt - 1 : $iAllowedCnt;
  354. $iValidSince = time();
  355. /**
  356. * Member is requesting/performing this action for the first time,
  357. * and there is no corresponding record in sys_acl_actions_track table.
  358. */
  359. if(!$aActionTrack) {
  360. $this->oDb->insertActionTarck($iActionId, $iProfileId, $iActionsLeft, $iValidSince);
  361. $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_ALLOWED;
  362. return $aResult;
  363. }
  364. /**
  365. * Action has been requested/performed at least once at this point and there is a corresponding record in sys_acl_actions_track table
  366. *
  367. * Action record in sys_acl_actions_track table is out of date.
  368. */
  369. $iPeriodEnd = (int)$aActionTrack['valid_since'] + $iPeriodLen * 3600; //ValidSince is in seconds, PeriodLen is in hours
  370. if($iPeriodLen > 0 && $iPeriodEnd < time()) {
  371. $this->oDb->updateActionTrack($iActionId, $iProfileId, $iActionsLeft, $iValidSince);
  372. $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_ALLOWED;
  373. return $aResult;
  374. }
  375. $iActionsLeft = (int)$aActionTrack['actions_left']; ///< Action record is up to date
  376. /**
  377. * Action limit reached for now
  378. */
  379. if($iActionsLeft <= 0){
  380. $aLangFileParams[CHECK_ACTION_LANG_FILE_LIMIT] = $iAllowedCnt;
  381. $aLangFileParams[CHECK_ACTION_LANG_FILE_PERIOD] = $iPeriodLen;
  382. $aLangFileParams[CHECK_ACTION_LANG_FILE_PERIOD_RESTART_AT] = bx_time_js($iPeriodEnd);
  383. $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_LIMIT_REACHED;
  384. $aResult[CHECK_ACTION_MESSAGE] = '<div class="bx-acl-err-msg">' . _t_ext(CHECK_ACTION_MESSAGE_LIMIT_REACHED, $aLangFileParams) . ($iPeriodLen > 0 ? _t_ext(CHECK_ACTION_MESSAGE_MESSAGE_EVERY_PERIOD, $aLangFileParams) : '') . '.</div>';
  385. return $aResult;
  386. }
  387. if($bPerformAction) {
  388. $iActionsLeft--;
  389. $this->oDb->updateActionTrack($iActionId, $iProfileId, $iActionsLeft);
  390. }
  391. }
  392. $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_ALLOWED;
  393. return $aResult;
  394. }
  395. /**
  396. * Get the number of allowed action
  397. *
  398. * @param int $iProfileId ID of a profile that is going to perform an action
  399. * @param int $iActionId ID of the action itself
  400. * @param boolean $bPerformAction if true, then action information is updated, i.e. action is 'performed'
  401. * @return int if the action is countable, or true if it's not countable
  402. */
  403. function getActionNumberLeft($iProfileId, $iActionId)
  404. {
  405. $aMembership = $this->getMemberMembershipInfo($iProfileId); // get current profile's membership information
  406. $aAction = $this->oDb->getAction($aMembership['id'], $iActionId);
  407. $iAllowedCnt = (int)$aAction['allowed_count']; ///< Number of allowed actions. Unlimited if not specified or 0
  408. if($iAllowedCnt > 0) {
  409. $aActionTrack = $this->oDb->getActionTrack($iActionId, $iProfileId);
  410. if(!$aActionTrack)
  411. return $iAllowedCnt;
  412. return (int)$aActionTrack['actions_left'];
  413. }
  414. return true;
  415. }
  416. /**
  417. * Get the list of existing memberships
  418. *
  419. * @param bool $bPurchasableOnly if true, fetches only purchasable memberships; 'purchasable' here means that:
  420. * - MemLevels.Purchasable = 'yes'
  421. * - MemLevels.Active = 'yes'
  422. * - there is at least one pricing option for the membership
  423. * @return array( membershipID_1 => membershipName_1, membershipID_2 => membershipName_2, ...) if no such memberships, then just array()
  424. */
  425. function getMemberships($bPurchasableOnly = false, $bActiveOnly = false, $isTranslate = true, $bFilterOutSystemAutomaticLevels = false)
  426. {
  427. $sType = 'all_pair';
  428. if($bPurchasableOnly)
  429. $sType = 'all_active_purchasble_pair';
  430. else if($bActiveOnly)
  431. $sType = 'all_active_pair';
  432. $aLevels = array();
  433. $this->oDb->getLevels(array('type' => $sType), $aLevels, false);
  434. if ($isTranslate)
  435. foreach ($aLevels as $k => $s)
  436. $aLevels[$k] = _t($s);
  437. if ($bFilterOutSystemAutomaticLevels) {
  438. unset($aLevels[MEMBERSHIP_ID_NON_MEMBER]);
  439. unset($aLevels[MEMBERSHIP_ID_ACCOUNT]);
  440. unset($aLevels[MEMBERSHIP_ID_UNCONFIRMED]);
  441. unset($aLevels[MEMBERSHIP_ID_PENDING]);
  442. unset($aLevels[MEMBERSHIP_ID_SUSPENDED]);
  443. }
  444. return $aLevels;
  445. }
  446. function getMembershipsBy($aParams)
  447. {
  448. $aLevels = array();
  449. $this->oDb->getLevels($aParams, $aLevels, false);
  450. return $aLevels;
  451. }
  452. /**
  453. * Get info about a given membership
  454. *
  455. * @param int $iLevelId membership to get info about
  456. * @return array(
  457. * 'id' => ID,
  458. * 'name' => name,
  459. * 'icon' => icon,
  460. * 'description' => description,
  461. * 'active' => active,
  462. * 'purchasable' => purchasable,
  463. * 'removable' => removable
  464. * 'quota_size' => quota size,
  465. * 'quota_number' => quota number,
  466. * 'quota_max_file_size' => quota max file size
  467. * )
  468. */
  469. function getMembershipInfo($iLevelId)
  470. {
  471. $aLevel = array();
  472. $this->oDb->getLevels(array('type' => 'by_id', 'value' => $iLevelId), $aLevel, false);
  473. return $aLevel;
  474. }
  475. /**
  476. * Retrieves information about membership for a given profile at a given moment.
  477. *
  478. * If there are no memberships purchased/assigned to the
  479. * given profile or all of them have expired at the given point,
  480. * the profile is assumed to be a standard profile, and the function
  481. * returns information about the Standard membership. This will
  482. * also happen if a profile wasnt actually registered in the database
  483. * at that point - the function will still return info about Standard
  484. * membership, not the Non-member one.
  485. *
  486. * If there is no profile with the given $iProfileId,
  487. * the function returns information about the Non-member or Authenticated
  488. * predefined membership.
  489. *
  490. * The Standard, Authenticated and Non-member memberships have their
  491. * DateStarts and DateExpires attributes set to NULL.
  492. *
  493. * @param int $iProfileId ID of a profile to get info about
  494. * @param int $time specifies the time to use when determining membership; if not specified, the function takes the current time
  495. * @return array(
  496. * 'id' => membership id,
  497. * 'name' => membership name,
  498. * 'date_starts' => (UNIX timestamp) date/time purchased,
  499. * 'date_expires' => (UNIX timestamp) date/time expires
  500. * )
  501. */
  502. function getMemberMembershipInfo($iProfileId, $iTime = 0, $bClearCache = 0)
  503. {
  504. $aMembershipCurrent = $this->getMemberMembershipInfoCurrent($iProfileId, $iTime, $bClearCache);
  505. if (isset($this->_aStandardMemberships[$aMembershipCurrent['id']]))
  506. return $aMembershipCurrent;
  507. $aMembership = $aMembershipCurrent;
  508. do {
  509. $iDateStarts = $aMembership['date_starts'];
  510. $aMembership = $this->getMemberMembershipInfoCurrent($iProfileId, ((int)$iDateStarts < 1 ? 0 : $iDateStarts - 1), $bClearCache);
  511. }
  512. while($aMembership['id'] == $aMembershipCurrent['id'] && (int)$aMembership['date_starts']);
  513. $aMembership = $aMembershipCurrent;
  514. do {
  515. $iDateExpires = $aMembership['date_expires'];
  516. $aMembership = $this->getMemberMembershipInfoCurrent($iProfileId, $iDateExpires, $bClearCache);
  517. } while($aMembership['id'] == $aMembershipCurrent['id'] && (int)$aMembership['date_expires']);
  518. $aMembershipCurrent['date_starts'] = $iDateStarts;
  519. $aMembershipCurrent['date_expires'] = $iDateExpires;
  520. return $aMembershipCurrent;
  521. }
  522. /**
  523. * Set a membership for a profile
  524. *
  525. * @param int $iProfileId profile that is going to get the membership
  526. * @param int $iLevelId membership that is going to be assigned to the profile
  527. * if $iLevelId == MEMBERSHIP_ID_STANDARD then $days and $bStartsNow parameters are not used,
  528. * so Standard membership is always set immediately and `forever`
  529. * @param mixed $mixedPeriod number of Days to set membership for or an array with 'period'-'period unit' pair. If number or 'period' in pair equal 0, then the membership is set forever
  530. * @param boolean $bStartsNow if true, the membership will start immediately if false, the membership will start after the current membership expires
  531. * @return boolean true in case of success, false in case of failure
  532. */
  533. function setMembership($iProfileId, $iLevelId, $mixedPeriod = 0, $bStartsNow = false, $sTransactionId = '')
  534. {
  535. $iProfileId = (int)$iProfileId;
  536. $iLevelId = (int)$iLevelId;
  537. $bStartsNow = $bStartsNow ? true : false;
  538. if (!$iProfileId)
  539. $iProfileId = -1;
  540. if (empty($sTransactionId))
  541. $sTransactionId = 'NULL';
  542. // check if profile exists
  543. if(($sProfileEmail = BxDolProfileQuery::getInstance()->getEmailById($iProfileId)) === false)
  544. return false;
  545. // check if membership exists
  546. $aLevel = array();
  547. $this->oDb->getLevels(array('type' => 'by_id', 'value' => $iLevelId), $aLevel, false);
  548. if(empty($aLevel) || !is_array($aLevel))
  549. return false;
  550. if($iLevelId == MEMBERSHIP_ID_NON_MEMBER)
  551. return false;
  552. $aMembershipCurrent = $this->getMemberMembershipInfo($iProfileId);
  553. $aMembershipLatest = $this->getMemberMembershipInfoLatest($iProfileId);
  554. // setting Standard membership level
  555. if ($iLevelId == MEMBERSHIP_ID_STANDARD) {
  556. if ($aMembershipCurrent['id'] == MEMBERSHIP_ID_STANDARD)
  557. return true;
  558. // delete present and future memberships
  559. $bResult = $this->oDb->deleteLevelByProfileId($iProfileId);
  560. if($bResult) {
  561. $this->oDb->cleanMemory('BxDolAclQuery::getLevelCurrent' . $iProfileId . time());
  562. unset(self::$_aCacheData[$iProfileId . '_0']);
  563. /**
  564. * @hooks
  565. * @hookdef hook-profile-unset_membership 'profile', 'unset_membership' - hook after a default membership level was set
  566. * - $unit_name - equals `profile`
  567. * - $action - equals `unset_membership`
  568. * - $object_id - not used
  569. * - $sender_id - profile id to set level to
  570. * - $extra_params - array of additional params with the following array keys:
  571. * - `mlevel` - [int] membership level id
  572. * @hook @ref hook-profile-unset_membership
  573. */
  574. bx_alert('profile', 'unset_membership', '', $iProfileId, [
  575. 'mlevel'=> $iLevelId
  576. ]);
  577. }
  578. return $bResult;
  579. }
  580. if ((is_numeric($mixedPeriod) && (int)$mixedPeriod < 0) || (is_array($mixedPeriod) && (!isset($mixedPeriod['period']) || $mixedPeriod['period'] < 0)))
  581. return false;
  582. /*
  583. * Make the membership starts after the latest membership expires or starts immediately
  584. * if latest membership is lifetime membership or immediate start was requested.
  585. */
  586. $iDateStarts = time();
  587. if ($bStartsNow || empty($aMembershipLatest['date_expires'])) {
  588. // Delete any profile's membership level and actions traces
  589. $this->oDb->deleteLevelByProfileId($iProfileId, true);
  590. $this->oDb->clearActionsTracksForMember($iProfileId);
  591. $this->oDb->cleanMemory('BxDolAclQuery::getLevelCurrent' . $iProfileId . time());
  592. unset(self::$_aCacheData[$iProfileId . '_0']);
  593. }
  594. else
  595. $iDateStarts = $aMembershipLatest['date_expires'];
  596. // set lifetime membership if 0 days is used.
  597. if(is_numeric($mixedPeriod))
  598. $mixedPeriod = array('period' => (int)$mixedPeriod, 'period_unit' => MEMBERSHIP_PERIOD_UNIT_DAY);
  599. if(!$this->oDb->insertLevelByProfileId($iProfileId, $iLevelId, $iDateStarts, $mixedPeriod, $sTransactionId))
  600. return false;
  601. $bProlong = $iLevelId == $aMembershipCurrent['id'];
  602. /**
  603. * @hooks
  604. * @hookdef hook-profile-set_membership 'profile', 'set_membership' - hook after a new membership level was set
  605. * - $unit_name - equals `profile`
  606. * - $action - equals `set_membership`
  607. * - $object_id - not used
  608. * - $sender_id - profile id to set level to
  609. * - $extra_params - array of additional params with the following array keys:
  610. * - `mlevel` - [int] membership level id
  611. * - `period` - [int] number of periods
  612. * - `period_unit` - [string] period unit (day, week, month, etc)
  613. * - `starts_now` - [boolean] if new membership starts immediately or queued
  614. * - `txn_id` - [string] related payment transaction id
  615. * @hook @ref hook-profile-set_membership
  616. */
  617. /**
  618. * @hooks
  619. * @hookdef hook-profile-prolong_membership 'profile', 'prolong_membership' - hook after current membership level was prolonged
  620. * It's equivalent to @ref hook-profile-set_membership
  621. * @hook @ref hook-profile-prolong_membership
  622. */
  623. bx_alert('profile', ($bProlong ? 'prolong' : 'set') . '_membership', '', $iProfileId, [
  624. 'mlevel'=> $iLevelId,
  625. 'period' => $mixedPeriod['period'],
  626. 'period_unit' => $mixedPeriod['period_unit'],
  627. 'starts_now' => $bStartsNow,
  628. 'txn_id' => $sTransactionId
  629. ]);
  630. // audit
  631. $aDataForAudit = array();
  632. if (!empty($aMembershipCurrent))
  633. $aDataForAudit = array('new_membership_level' => _t($aLevel['name']), 'old_membership_level' => _t($aMembershipCurrent['name']));
  634. BxDolProfile::getInstance($iProfileId)->doAudit('_sys_audit_action_set_membership', $aDataForAudit);
  635. $oEmailTemplates = BxDolEmailTemplates::getInstance();
  636. // Check if there is a special email template for this membership level
  637. $sTemplateName = 't_Mem' . ($bProlong ? 'Prolonged' : 'Changed') . '_' . $aLevel['id'];
  638. if ($oEmailTemplates->getTemplate($sTemplateName) === FALSE)
  639. $sTemplateName = 't_Mem' . ($bProlong ? 'Prolonged' : 'Changed');
  640. // Send notification
  641. $aTemplate = $oEmailTemplates->parseTemplate($sTemplateName, array('membership_level' => _t($aLevel['name'])), 0, $iProfileId);
  642. if ($aTemplate)
  643. sendMail($sProfileEmail, $aTemplate['Subject'], $aTemplate['Body']);
  644. return true;
  645. }
  646. function unsetMembership($iProfileId, $iLevelId, $sTransactionId)
  647. {
  648. return $this->oDb->deleteLevelBy(array(
  649. 'IDMember' => $iProfileId,
  650. 'IDLevel' => $iLevelId,
  651. 'TransactionID' => $sTransactionId
  652. ));
  653. }
  654. /**
  655. * get action id by module and name
  656. * @param $sAction action name
  657. * @param $sModule module name
  658. * @param $aActions array of actions from sys_acl_actions table, with default array keys (starting from 0) and text values
  659. */
  660. function getMembershipActionId($sAction, $sModule)
  661. {
  662. $this->oDb->getActions(array('type' => 'by_names_and_module', 'value' => $sAction, 'module' => $sModule), $aActions, false);
  663. if (count($aActions) > 1)
  664. trigger_error('Duplicate action - name:' . $sAction . ', module:' . $sModule, E_USER_ERROR);
  665. $aAction = array_pop($aActions);
  666. return $aAction['id'];
  667. }
  668. function getExpirationLetter($iProfileId, $sLevelName, $iLevelExpireDays )
  669. {
  670. $iProfileId = (int)$iProfileId;
  671. if(!$iProfileId)
  672. return false;
  673. $oProfileQuery = BxDolProfileQuery::getInstance();
  674. $sProfileEmail = $oProfileQuery->getEmailById($iProfileId);
  675. $aPlus = array(
  676. 'membership_name' => _t($sLevelName),
  677. 'expire_days' => $iLevelExpireDays,
  678. 'page_url' => BxDolRequest::serviceExists('bx_acl', 'get_view_url') ? BxDolService::call('bx_acl', 'get_view_url') : '#'
  679. );
  680. $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_MemExpiration', $aPlus, 0, $iProfileId);
  681. $iResult = $aTemplate && sendMail($sProfileEmail, $aTemplate['Subject'], $aTemplate['Body'], $iProfileId, $aPlus);
  682. return !empty($iResult);
  683. }
  684. /**
  685. * clear expired membership levels
  686. */
  687. public function maintenance()
  688. {
  689. return $this->oDb->maintenance();
  690. }
  691. protected function getMemberMembershipInfoCurrent($iProfileId, $iTime = 0, $bClearCache = 0)
  692. {
  693. $sKey = $iProfileId . '_' . $iTime;
  694. if ($bClearCache && isset(self::$_aCacheData[$sKey]))
  695. unset(self::$_aCacheData[$sKey]);
  696. elseif (array_key_exists($sKey, self::$_aCacheData) && !defined('BX_DOL_INSTALL') && !defined('BX_DOL_CRON_EXECUTE'))
  697. return self::$_aCacheData[$sKey];
  698. $aMemLevel = false;
  699. do {
  700. // get profile status
  701. $oProfile = BxDolProfile::getInstance($iProfileId);
  702. $aProfileInfo = $oProfile ? $oProfile->getInfo() : false;
  703. $sProfileStatus = $aProfileInfo ? $aProfileInfo['status'] : false;
  704. $sProfileType = $aProfileInfo ? $aProfileInfo['type'] : false;
  705. // account profile
  706. if($sProfileType == 'system') {
  707. $aMemLevel = $this->oDb->getLevelByIdCached(MEMBERSHIP_ID_ACCOUNT);
  708. if (!$aMemLevel)
  709. trigger_error ('Standard member level is missing: MEMBERSHIP_ID_ACCOUNT', E_USER_ERROR);
  710. break;
  711. }
  712. // check if account is unconfirmed, every account's profile is unconfirmed if account is unconfirmed
  713. $oAccount = $aProfileInfo ? BxDolAccount::getInstance($aProfileInfo['account_id']) : false;
  714. if ($oAccount && !$oAccount->isConfirmed()) {
  715. $aMemLevel = $this->oDb->getLevelByIdCached(MEMBERSHIP_ID_UNCONFIRMED);
  716. if (!$aMemLevel)
  717. trigger_error ('Standard member level is missing: MEMBERSHIP_ID_UNCONFIRMED', E_USER_ERROR);
  718. break;
  719. }
  720. // profile is not active, so return standard memberships according to profile status
  721. if (false === $sProfileStatus || BX_PROFILE_STATUS_ACTIVE != $sProfileStatus) {
  722. if (!isset($this->_aProfileStatus2LevelMap[$sProfileStatus]))
  723. $iLevelId = MEMBERSHIP_ID_NON_MEMBER; // if there is no profile status - then it isn't member
  724. else
  725. $iLevelId = $this->_aProfileStatus2LevelMap[$sProfileStatus]; // get member level id which associated with every non-active status
  726. $aMemLevel = $this->oDb->getLevelByIdCached($iLevelId);
  727. if (!$aMemLevel)
  728. trigger_error ('Standard member level is missing: ' . $iLevelId, E_USER_ERROR);
  729. break;
  730. }
  731. // profile is active get memebr level from profile
  732. $aMemLevel = $this->oDb->getLevelCurrent((int)$iProfileId, $iTime);
  733. // There are no purchased/assigned memberships for the profile or all of them have expired.
  734. // In this case the profile is assumed to have Standard membership.
  735. if (!$aMemLevel || is_null($aMemLevel['id'])) {
  736. $aMemLevel = $this->oDb->getLevelByIdCached(MEMBERSHIP_ID_STANDARD);
  737. if (!$aMemLevel)
  738. trigger_error ('Standard member level is missing: ' . MEMBERSHIP_ID_NON_MEMBER, E_USER_ERROR);
  739. break;
  740. }
  741. }
  742. while (0);
  743. return (self::$_aCacheData[$sKey] = $aMemLevel);
  744. }
  745. protected function getMemberMembershipInfoLatest($iProfileId, $iTime = 0, $bClearCache = 0)
  746. {
  747. $aMembershipCurrent = $this->getMemberMembershipInfoCurrent($iProfileId, $iTime, $bClearCache);
  748. if (isset($this->_aStandardMemberships[$aMembershipCurrent['id']]))
  749. return $aMembershipCurrent;
  750. $aMembership = $aMembershipCurrent;
  751. while ($aMembership['id'] != MEMBERSHIP_ID_STANDARD) {
  752. $aMembershipLast = $aMembership;
  753. if(!isset($aMembership['date_expires']) || (int)$aMembership['date_expires'] == 0)
  754. break;
  755. $aMembership = $this->getMemberMembershipInfoCurrent($iProfileId, $aMembership['date_expires'], $bClearCache);
  756. }
  757. return $aMembershipLast;
  758. }
  759. public function onProfileDelete ($iProfileId)
  760. {
  761. return $this->oDb->deleteLevelByProfileId($iProfileId, true);
  762. }
  763. }
  764. function checkAction($iProfileId, $iActionId, $bPerformAction = false)
  765. {
  766. return BxDolAcl::getInstance()->checkAction($iProfileId, $iActionId, $bPerformAction);
  767. }
  768. function checkActionModule($iProfileId, $sActionName, $sModuleName, $bPerformAction = false)
  769. {
  770. $oACL = BxDolAcl::getInstance();
  771. $iActionId = $oACL->getMembershipActionId($sActionName, $sModuleName);
  772. if (!$iActionId)
  773. bx_trigger_error("Unknown action: '$sActionName' in module '$sModuleName'", 1);
  774. return $oACL->checkAction($iProfileId, $iActionId, $bPerformAction);
  775. }
  776. function getActionNumberLeftModule($iProfileId, $sActionName, $sModuleName)
  777. {
  778. $oACL = BxDolAcl::getInstance();
  779. $iActionId = $oACL->getMembershipActionId($sActionName, $sModuleName);
  780. if (!$iActionId)
  781. bx_trigger_error("Unknown action: '$sActionName' in module '$sModuleName'", 1);
  782. return $oACL->getActionNumberLeft($iProfileId, $iActionId);
  783. }
  784. /** @} */