BxDolAcl.php 40 KB

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