PageRenderTime 51ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/availability/condition/profile/classes/condition.php

https://bitbucket.org/moodle/moodle
PHP | 618 lines | 420 code | 46 blank | 152 comment | 84 complexity | 2ca4e6bf27d7826c2b0a9f24cff8ac1b MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * User profile field condition.
  18. *
  19. * @package availability_profile
  20. * @copyright 2014 The Open University
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. namespace availability_profile;
  24. defined('MOODLE_INTERNAL') || die();
  25. /**
  26. * User profile field condition.
  27. *
  28. * @package availability_profile
  29. * @copyright 2014 The Open University
  30. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  31. */
  32. class condition extends \core_availability\condition {
  33. /** @var string Operator: field contains value */
  34. const OP_CONTAINS = 'contains';
  35. /** @var string Operator: field does not contain value */
  36. const OP_DOES_NOT_CONTAIN = 'doesnotcontain';
  37. /** @var string Operator: field equals value */
  38. const OP_IS_EQUAL_TO = 'isequalto';
  39. /** @var string Operator: field starts with value */
  40. const OP_STARTS_WITH = 'startswith';
  41. /** @var string Operator: field ends with value */
  42. const OP_ENDS_WITH = 'endswith';
  43. /** @var string Operator: field is empty */
  44. const OP_IS_EMPTY = 'isempty';
  45. /** @var string Operator: field is not empty */
  46. const OP_IS_NOT_EMPTY = 'isnotempty';
  47. /** @var array|null Array of custom profile fields (static cache within request) */
  48. protected static $customprofilefields = null;
  49. /** @var string Field name (for standard fields) or '' if custom field */
  50. protected $standardfield = '';
  51. /** @var int Field name (for custom fields) or '' if standard field */
  52. protected $customfield = '';
  53. /** @var string Operator type (OP_xx constant) */
  54. protected $operator;
  55. /** @var string Expected value for field */
  56. protected $value = '';
  57. /**
  58. * Constructor.
  59. *
  60. * @param \stdClass $structure Data structure from JSON decode
  61. * @throws \coding_exception If invalid data structure.
  62. */
  63. public function __construct($structure) {
  64. // Get operator.
  65. if (isset($structure->op) && in_array($structure->op, array(self::OP_CONTAINS,
  66. self::OP_DOES_NOT_CONTAIN, self::OP_IS_EQUAL_TO, self::OP_STARTS_WITH,
  67. self::OP_ENDS_WITH, self::OP_IS_EMPTY, self::OP_IS_NOT_EMPTY), true)) {
  68. $this->operator = $structure->op;
  69. } else {
  70. throw new \coding_exception('Missing or invalid ->op for profile condition');
  71. }
  72. // For operators other than the empty/not empty ones, require value.
  73. switch($this->operator) {
  74. case self::OP_IS_EMPTY:
  75. case self::OP_IS_NOT_EMPTY:
  76. if (isset($structure->v)) {
  77. throw new \coding_exception('Unexpected ->v for non-value operator');
  78. }
  79. break;
  80. default:
  81. if (isset($structure->v) && is_string($structure->v)) {
  82. $this->value = $structure->v;
  83. } else {
  84. throw new \coding_exception('Missing or invalid ->v for profile condition');
  85. }
  86. break;
  87. }
  88. // Get field type.
  89. if (property_exists($structure, 'sf')) {
  90. if (property_exists($structure, 'cf')) {
  91. throw new \coding_exception('Both ->sf and ->cf for profile condition');
  92. }
  93. if (is_string($structure->sf)) {
  94. $this->standardfield = $structure->sf;
  95. } else {
  96. throw new \coding_exception('Invalid ->sf for profile condition');
  97. }
  98. } else if (property_exists($structure, 'cf')) {
  99. if (is_string($structure->cf)) {
  100. $this->customfield = $structure->cf;
  101. } else {
  102. throw new \coding_exception('Invalid ->cf for profile condition');
  103. }
  104. } else {
  105. throw new \coding_exception('Missing ->sf or ->cf for profile condition');
  106. }
  107. }
  108. public function save() {
  109. $result = (object)array('type' => 'profile', 'op' => $this->operator);
  110. if ($this->customfield) {
  111. $result->cf = $this->customfield;
  112. } else {
  113. $result->sf = $this->standardfield;
  114. }
  115. switch($this->operator) {
  116. case self::OP_IS_EMPTY:
  117. case self::OP_IS_NOT_EMPTY:
  118. break;
  119. default:
  120. $result->v = $this->value;
  121. break;
  122. }
  123. return $result;
  124. }
  125. /**
  126. * Returns a JSON object which corresponds to a condition of this type.
  127. *
  128. * Intended for unit testing, as normally the JSON values are constructed
  129. * by JavaScript code.
  130. *
  131. * @param bool $customfield True if this is a custom field
  132. * @param string $fieldname Field name
  133. * @param string $operator Operator name (OP_xx constant)
  134. * @param string|null $value Value (not required for some operator types)
  135. * @return stdClass Object representing condition
  136. */
  137. public static function get_json($customfield, $fieldname, $operator, $value = null) {
  138. $result = (object)array('type' => 'profile', 'op' => $operator);
  139. if ($customfield) {
  140. $result->cf = $fieldname;
  141. } else {
  142. $result->sf = $fieldname;
  143. }
  144. switch ($operator) {
  145. case self::OP_IS_EMPTY:
  146. case self::OP_IS_NOT_EMPTY:
  147. break;
  148. default:
  149. if (is_null($value)) {
  150. throw new \coding_exception('Operator requires value');
  151. }
  152. $result->v = $value;
  153. break;
  154. }
  155. return $result;
  156. }
  157. public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
  158. $uservalue = $this->get_cached_user_profile_field($userid);
  159. $allow = self::is_field_condition_met($this->operator, $uservalue, $this->value);
  160. if ($not) {
  161. $allow = !$allow;
  162. }
  163. return $allow;
  164. }
  165. public function get_description($full, $not, \core_availability\info $info) {
  166. $course = $info->get_course();
  167. // Display the fieldname into current lang.
  168. if ($this->customfield) {
  169. // Is a custom profile field (will use multilang).
  170. $customfields = self::get_custom_profile_fields();
  171. if (array_key_exists($this->customfield, $customfields)) {
  172. $translatedfieldname = $customfields[$this->customfield]->name;
  173. } else {
  174. $translatedfieldname = get_string('missing', 'availability_profile',
  175. $this->customfield);
  176. }
  177. } else {
  178. $translatedfieldname = \core_user\fields::get_display_name($this->standardfield);
  179. }
  180. $a = new \stdClass();
  181. // Not safe to call format_string here; use the special function to call it later.
  182. $a->field = self::description_format_string($translatedfieldname);
  183. $a->value = s($this->value);
  184. if ($not) {
  185. // When doing NOT strings, we replace the operator with its inverse.
  186. // Some of them don't have inverses, so for those we use a new
  187. // identifier which is only used for this lang string.
  188. switch($this->operator) {
  189. case self::OP_CONTAINS:
  190. $opname = self::OP_DOES_NOT_CONTAIN;
  191. break;
  192. case self::OP_DOES_NOT_CONTAIN:
  193. $opname = self::OP_CONTAINS;
  194. break;
  195. case self::OP_ENDS_WITH:
  196. $opname = 'notendswith';
  197. break;
  198. case self::OP_IS_EMPTY:
  199. $opname = self::OP_IS_NOT_EMPTY;
  200. break;
  201. case self::OP_IS_EQUAL_TO:
  202. $opname = 'notisequalto';
  203. break;
  204. case self::OP_IS_NOT_EMPTY:
  205. $opname = self::OP_IS_EMPTY;
  206. break;
  207. case self::OP_STARTS_WITH:
  208. $opname = 'notstartswith';
  209. break;
  210. default:
  211. throw new \coding_exception('Unexpected operator: ' . $this->operator);
  212. }
  213. } else {
  214. $opname = $this->operator;
  215. }
  216. return get_string('requires_' . $opname, 'availability_profile', $a);
  217. }
  218. protected function get_debug_string() {
  219. if ($this->customfield) {
  220. $out = '*' . $this->customfield;
  221. } else {
  222. $out = $this->standardfield;
  223. }
  224. $out .= ' ' . $this->operator;
  225. switch($this->operator) {
  226. case self::OP_IS_EMPTY:
  227. case self::OP_IS_NOT_EMPTY:
  228. break;
  229. default:
  230. $out .= ' ' . $this->value;
  231. break;
  232. }
  233. return $out;
  234. }
  235. /**
  236. * Returns true if a field meets the required conditions, false otherwise.
  237. *
  238. * @param string $operator the requirement/condition
  239. * @param string $uservalue the user's value
  240. * @param string $value the value required
  241. * @return boolean True if conditions are met
  242. */
  243. protected static function is_field_condition_met($operator, $uservalue, $value) {
  244. if ($uservalue === false) {
  245. // If the user value is false this is an instant fail.
  246. // All user values come from the database as either data or the default.
  247. // They will always be a string.
  248. return false;
  249. }
  250. $fieldconditionmet = true;
  251. // Just to be doubly sure it is a string.
  252. $uservalue = (string)$uservalue;
  253. switch($operator) {
  254. case self::OP_CONTAINS:
  255. $pos = strpos($uservalue, $value);
  256. if ($pos === false) {
  257. $fieldconditionmet = false;
  258. }
  259. break;
  260. case self::OP_DOES_NOT_CONTAIN:
  261. if (!empty($value)) {
  262. $pos = strpos($uservalue, $value);
  263. if ($pos !== false) {
  264. $fieldconditionmet = false;
  265. }
  266. }
  267. break;
  268. case self::OP_IS_EQUAL_TO:
  269. if ($value !== $uservalue) {
  270. $fieldconditionmet = false;
  271. }
  272. break;
  273. case self::OP_STARTS_WITH:
  274. $length = strlen($value);
  275. if ((substr($uservalue, 0, $length) !== $value)) {
  276. $fieldconditionmet = false;
  277. }
  278. break;
  279. case self::OP_ENDS_WITH:
  280. $length = strlen($value);
  281. $start = $length * -1;
  282. if (substr($uservalue, $start) !== $value) {
  283. $fieldconditionmet = false;
  284. }
  285. break;
  286. case self::OP_IS_EMPTY:
  287. if (!empty($uservalue)) {
  288. $fieldconditionmet = false;
  289. }
  290. break;
  291. case self::OP_IS_NOT_EMPTY:
  292. if (empty($uservalue)) {
  293. $fieldconditionmet = false;
  294. }
  295. break;
  296. }
  297. return $fieldconditionmet;
  298. }
  299. /**
  300. * Gets data about custom profile fields. Cached statically in current
  301. * request.
  302. *
  303. * This only includes fields which can be tested by the system (those whose
  304. * data is cached in $USER object) - basically doesn't include textarea type
  305. * fields.
  306. *
  307. * @return array Array of records indexed by shortname
  308. */
  309. public static function get_custom_profile_fields() {
  310. global $DB, $CFG;
  311. if (self::$customprofilefields === null) {
  312. // Get fields and store them indexed by shortname.
  313. require_once($CFG->dirroot . '/user/profile/lib.php');
  314. $fields = profile_get_custom_fields(true);
  315. self::$customprofilefields = array();
  316. foreach ($fields as $field) {
  317. self::$customprofilefields[$field->shortname] = $field;
  318. }
  319. }
  320. return self::$customprofilefields;
  321. }
  322. /**
  323. * Wipes the static cache (for use in unit tests).
  324. */
  325. public static function wipe_static_cache() {
  326. self::$customprofilefields = null;
  327. }
  328. /**
  329. * Return the value for a user's profile field
  330. *
  331. * @param int $userid User ID
  332. * @return string|bool Value, or false if user does not have a value for this field
  333. */
  334. protected function get_cached_user_profile_field($userid) {
  335. global $USER, $DB, $CFG;
  336. $iscurrentuser = $USER->id == $userid;
  337. if (isguestuser($userid) || ($iscurrentuser && !isloggedin())) {
  338. // Must be logged in and can't be the guest.
  339. return false;
  340. }
  341. // Custom profile fields will be numeric, there are no numeric standard profile fields so this is not a problem.
  342. $iscustomprofilefield = $this->customfield ? true : false;
  343. if ($iscustomprofilefield) {
  344. // As its a custom profile field we need to map the id back to the actual field.
  345. // We'll also preload all of the other custom profile fields just in case and ensure we have the
  346. // default value available as well.
  347. if (!array_key_exists($this->customfield, self::get_custom_profile_fields())) {
  348. // No such field exists.
  349. // This shouldn't normally happen but occur if things go wrong when deleting a custom profile field
  350. // or when restoring a backup of a course with user profile field conditions.
  351. return false;
  352. }
  353. $field = $this->customfield;
  354. } else {
  355. $field = $this->standardfield;
  356. }
  357. // If its the current user than most likely we will be able to get this information from $USER.
  358. // If its a regular profile field then it should already be available, if not then we have a mega problem.
  359. // If its a custom profile field then it should be available but may not be. If it is then we use the value
  360. // available, otherwise we load all custom profile fields into a temp object and refer to that.
  361. // Noting its not going be great for performance if we have to use the temp object as it involves loading the
  362. // custom profile field API and classes.
  363. if ($iscurrentuser) {
  364. if (!$iscustomprofilefield) {
  365. if (property_exists($USER, $field)) {
  366. return $USER->{$field};
  367. } else {
  368. // Unknown user field. This should not happen.
  369. throw new \coding_exception('Requested user profile field does not exist');
  370. }
  371. }
  372. // Checking if the custom profile fields are already available.
  373. if (!isset($USER->profile)) {
  374. // Drat! they're not. We need to use a temp object and load them.
  375. // We don't use $USER as the profile fields are loaded into the object.
  376. $user = new \stdClass;
  377. $user->id = $USER->id;
  378. // This should ALWAYS be set, but just in case we check.
  379. require_once($CFG->dirroot . '/user/profile/lib.php');
  380. profile_load_custom_fields($user);
  381. if (array_key_exists($field, $user->profile)) {
  382. return $user->profile[$field];
  383. }
  384. } else if (array_key_exists($field, $USER->profile)) {
  385. // Hurrah they're available, this is easy.
  386. return $USER->profile[$field];
  387. }
  388. // The profile field doesn't exist.
  389. return false;
  390. } else {
  391. // Loading for another user.
  392. if ($iscustomprofilefield) {
  393. // Fetch the data for the field. Noting we keep this query simple so that Database caching takes care of performance
  394. // for us (this will likely be hit again).
  395. // We are able to do this because we've already pre-loaded the custom fields.
  396. $data = $DB->get_field('user_info_data', 'data', array('userid' => $userid,
  397. 'fieldid' => self::$customprofilefields[$field]->id), IGNORE_MISSING);
  398. // If we have data return that, otherwise return the default.
  399. if ($data !== false) {
  400. return $data;
  401. } else {
  402. return self::$customprofilefields[$field]->defaultdata;
  403. }
  404. } else {
  405. // Its a standard field, retrieve it from the user.
  406. return $DB->get_field('user', $field, array('id' => $userid), MUST_EXIST);
  407. }
  408. }
  409. return false;
  410. }
  411. public function is_applied_to_user_lists() {
  412. // Profile conditions are assumed to be 'permanent', so they affect the
  413. // display of user lists for activities.
  414. return true;
  415. }
  416. public function filter_user_list(array $users, $not, \core_availability\info $info,
  417. \core_availability\capability_checker $checker) {
  418. global $CFG, $DB;
  419. // If the array is empty already, just return it.
  420. if (!$users) {
  421. return $users;
  422. }
  423. // Get all users from the list who match the condition.
  424. list ($sql, $params) = $DB->get_in_or_equal(array_keys($users));
  425. if ($this->customfield) {
  426. $customfields = self::get_custom_profile_fields();
  427. if (!array_key_exists($this->customfield, $customfields)) {
  428. // If the field isn't found, nobody matches.
  429. return array();
  430. }
  431. $customfield = $customfields[$this->customfield];
  432. // Fetch custom field value for all users.
  433. $values = $DB->get_records_select('user_info_data', 'fieldid = ? AND userid ' . $sql,
  434. array_merge(array($customfield->id), $params),
  435. '', 'userid, data');
  436. $valuefield = 'data';
  437. $default = $customfield->defaultdata;
  438. } else {
  439. $values = $DB->get_records_select('user', 'id ' . $sql, $params,
  440. '', 'id, '. $this->standardfield);
  441. $valuefield = $this->standardfield;
  442. $default = '';
  443. }
  444. // Filter the user list.
  445. $result = array();
  446. foreach ($users as $id => $user) {
  447. // Get value for user.
  448. if (array_key_exists($id, $values)) {
  449. $value = $values[$id]->{$valuefield};
  450. } else {
  451. $value = $default;
  452. }
  453. // Check value.
  454. $allow = $this->is_field_condition_met($this->operator, $value, $this->value);
  455. if ($not) {
  456. $allow = !$allow;
  457. }
  458. if ($allow) {
  459. $result[$id] = $user;
  460. }
  461. }
  462. return $result;
  463. }
  464. /**
  465. * Gets SQL to match a field against this condition. The second copy of the
  466. * field is in case you're using variables for the field so that it needs
  467. * to be two different ones.
  468. *
  469. * @param string $field Field name
  470. * @param string $field2 Second copy of field name (default same).
  471. * @param boolean $istext Any of the fields correspond to a TEXT column in database (true) or not (false).
  472. * @return array Array of SQL and parameters
  473. */
  474. private function get_condition_sql($field, $field2 = null, $istext = false) {
  475. global $DB;
  476. if (is_null($field2)) {
  477. $field2 = $field;
  478. }
  479. $params = array();
  480. switch($this->operator) {
  481. case self::OP_CONTAINS:
  482. $sql = $DB->sql_like($field, self::unique_sql_parameter(
  483. $params, '%' . $this->value . '%'));
  484. break;
  485. case self::OP_DOES_NOT_CONTAIN:
  486. if (empty($this->value)) {
  487. // The 'does not contain nothing' expression matches everyone.
  488. return null;
  489. }
  490. $sql = $DB->sql_like($field, self::unique_sql_parameter(
  491. $params, '%' . $this->value . '%'), true, true, true);
  492. break;
  493. case self::OP_IS_EQUAL_TO:
  494. if ($istext) {
  495. $sql = $DB->sql_compare_text($field) . ' = ' . $DB->sql_compare_text(
  496. self::unique_sql_parameter($params, $this->value));
  497. } else {
  498. $sql = $field . ' = ' . self::unique_sql_parameter(
  499. $params, $this->value);
  500. }
  501. break;
  502. case self::OP_STARTS_WITH:
  503. $sql = $DB->sql_like($field, self::unique_sql_parameter(
  504. $params, $this->value . '%'));
  505. break;
  506. case self::OP_ENDS_WITH:
  507. $sql = $DB->sql_like($field, self::unique_sql_parameter(
  508. $params, '%' . $this->value));
  509. break;
  510. case self::OP_IS_EMPTY:
  511. // Mimic PHP empty() behaviour for strings, '0' or ''.
  512. $emptystring = self::unique_sql_parameter($params, '');
  513. if ($istext) {
  514. $sql = '(' . $DB->sql_compare_text($field) . " IN ('0', $emptystring) OR $field2 IS NULL)";
  515. } else {
  516. $sql = '(' . $field . " IN ('0', $emptystring) OR $field2 IS NULL)";
  517. }
  518. break;
  519. case self::OP_IS_NOT_EMPTY:
  520. $emptystring = self::unique_sql_parameter($params, '');
  521. if ($istext) {
  522. $sql = '(' . $DB->sql_compare_text($field) . " NOT IN ('0', $emptystring) AND $field2 IS NOT NULL)";
  523. } else {
  524. $sql = '(' . $field . " NOT IN ('0', $emptystring) AND $field2 IS NOT NULL)";
  525. }
  526. break;
  527. }
  528. return array($sql, $params);
  529. }
  530. public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) {
  531. global $DB;
  532. // Build suitable SQL depending on custom or standard field.
  533. if ($this->customfield) {
  534. $customfields = self::get_custom_profile_fields();
  535. if (!array_key_exists($this->customfield, $customfields)) {
  536. // If the field isn't found, nobody matches.
  537. return array('SELECT id FROM {user} WHERE 0 = 1', array());
  538. }
  539. $customfield = $customfields[$this->customfield];
  540. $mainparams = array();
  541. $tablesql = "LEFT JOIN {user_info_data} ud ON ud.fieldid = " .
  542. self::unique_sql_parameter($mainparams, $customfield->id) .
  543. " AND ud.userid = userids.id";
  544. list ($condition, $conditionparams) = $this->get_condition_sql('ud.data', null, true);
  545. $mainparams = array_merge($mainparams, $conditionparams);
  546. // If default is true, then allow that too.
  547. if ($this->is_field_condition_met(
  548. $this->operator, $customfield->defaultdata, $this->value)) {
  549. $where = "((ud.data IS NOT NULL AND $condition) OR (ud.data IS NULL))";
  550. } else {
  551. $where = "(ud.data IS NOT NULL AND $condition)";
  552. }
  553. } else {
  554. $tablesql = "JOIN {user} u ON u.id = userids.id";
  555. list ($where, $mainparams) = $this->get_condition_sql(
  556. 'u.' . $this->standardfield);
  557. }
  558. // Handle NOT.
  559. if ($not) {
  560. $where = 'NOT (' . $where . ')';
  561. }
  562. // Get enrolled user SQL and combine with this query.
  563. list ($enrolsql, $enrolparams) =
  564. get_enrolled_sql($info->get_context(), '', 0, $onlyactive);
  565. $sql = "SELECT userids.id
  566. FROM ($enrolsql) userids
  567. $tablesql
  568. WHERE $where";
  569. $params = array_merge($enrolparams, $mainparams);
  570. return array($sql, $params);
  571. }
  572. }