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

855 line
25KB

  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\rbac;
  8. use yii\base\InvalidCallException;
  9. use yii\base\InvalidParamException;
  10. use Yii;
  11. use yii\helpers\VarDumper;
  12. /**
  13. * PhpManager represents an authorization manager that stores authorization
  14. * information in terms of a PHP script file.
  15. *
  16. * The authorization data will be saved to and loaded from three files
  17. * specified by [[itemFile]], [[assignmentFile]] and [[ruleFile]].
  18. *
  19. * PhpManager is mainly suitable for authorization data that is not too big
  20. * (for example, the authorization data for a personal blog system).
  21. * Use [[DbManager]] for more complex authorization data.
  22. *
  23. * Note that PhpManager is not compatible with facebooks [HHVM](http://hhvm.com/) because
  24. * it relies on writing php files and including them afterwards which is not supported by HHVM.
  25. *
  26. * @author Qiang Xue <qiang.xue@gmail.com>
  27. * @author Alexander Kochetov <creocoder@gmail.com>
  28. * @author Christophe Boulain <christophe.boulain@gmail.com>
  29. * @author Alexander Makarov <sam@rmcreative.ru>
  30. * @since 2.0
  31. */
  32. class PhpManager extends BaseManager
  33. {
  34. /**
  35. * @var string the path of the PHP script that contains the authorization items.
  36. * This can be either a file path or a path alias to the file.
  37. * Make sure this file is writable by the Web server process if the authorization needs to be changed online.
  38. * @see loadFromFile()
  39. * @see saveToFile()
  40. */
  41. public $itemFile = '@app/rbac/items.php';
  42. /**
  43. * @var string the path of the PHP script that contains the authorization assignments.
  44. * This can be either a file path or a path alias to the file.
  45. * Make sure this file is writable by the Web server process if the authorization needs to be changed online.
  46. * @see loadFromFile()
  47. * @see saveToFile()
  48. */
  49. public $assignmentFile = '@app/rbac/assignments.php';
  50. /**
  51. * @var string the path of the PHP script that contains the authorization rules.
  52. * This can be either a file path or a path alias to the file.
  53. * Make sure this file is writable by the Web server process if the authorization needs to be changed online.
  54. * @see loadFromFile()
  55. * @see saveToFile()
  56. */
  57. public $ruleFile = '@app/rbac/rules.php';
  58. /**
  59. * @var Item[]
  60. */
  61. protected $items = []; // itemName => item
  62. /**
  63. * @var array
  64. */
  65. protected $children = []; // itemName, childName => child
  66. /**
  67. * @var array
  68. */
  69. protected $assignments = []; // userId, itemName => assignment
  70. /**
  71. * @var Rule[]
  72. */
  73. protected $rules = []; // ruleName => rule
  74. /**
  75. * Initializes the application component.
  76. * This method overrides parent implementation by loading the authorization data
  77. * from PHP script.
  78. */
  79. public function init()
  80. {
  81. parent::init();
  82. $this->itemFile = Yii::getAlias($this->itemFile);
  83. $this->assignmentFile = Yii::getAlias($this->assignmentFile);
  84. $this->ruleFile = Yii::getAlias($this->ruleFile);
  85. $this->load();
  86. }
  87. /**
  88. * @inheritdoc
  89. */
  90. public function checkAccess($userId, $permissionName, $params = [])
  91. {
  92. $assignments = $this->getAssignments($userId);
  93. return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments);
  94. }
  95. /**
  96. * @inheritdoc
  97. */
  98. public function getAssignments($userId)
  99. {
  100. return isset($this->assignments[$userId]) ? $this->assignments[$userId] : [];
  101. }
  102. /**
  103. * Performs access check for the specified user.
  104. * This method is internally called by [[checkAccess()]].
  105. *
  106. * @param string|integer $user the user ID. This should can be either an integer or a string representing
  107. * the unique identifier of a user. See [[\yii\web\User::id]].
  108. * @param string $itemName the name of the operation that need access check
  109. * @param array $params name-value pairs that would be passed to rules associated
  110. * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
  111. * which holds the value of `$userId`.
  112. * @param Assignment[] $assignments the assignments to the specified user
  113. * @return boolean whether the operations can be performed by the user.
  114. */
  115. protected function checkAccessRecursive($user, $itemName, $params, $assignments)
  116. {
  117. if (!isset($this->items[$itemName])) {
  118. return false;
  119. }
  120. /* @var $item Item */
  121. $item = $this->items[$itemName];
  122. Yii::trace($item instanceof Role ? "Checking role: $itemName" : "Checking permission : $itemName", __METHOD__);
  123. if (!$this->executeRule($user, $item, $params)) {
  124. return false;
  125. }
  126. if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
  127. return true;
  128. }
  129. foreach ($this->children as $parentName => $children) {
  130. if (isset($children[$itemName]) && $this->checkAccessRecursive($user, $parentName, $params, $assignments)) {
  131. return true;
  132. }
  133. }
  134. return false;
  135. }
  136. /**
  137. * @inheritdoc
  138. * @since 2.0.8
  139. */
  140. public function canAddChild($parent, $child)
  141. {
  142. return !$this->detectLoop($parent, $child);
  143. }
  144. /**
  145. * @inheritdoc
  146. */
  147. public function addChild($parent, $child)
  148. {
  149. if (!isset($this->items[$parent->name], $this->items[$child->name])) {
  150. throw new InvalidParamException("Either '{$parent->name}' or '{$child->name}' does not exist.");
  151. }
  152. if ($parent->name === $child->name) {
  153. throw new InvalidParamException("Cannot add '{$parent->name} ' as a child of itself.");
  154. }
  155. if ($parent instanceof Permission && $child instanceof Role) {
  156. throw new InvalidParamException('Cannot add a role as a child of a permission.');
  157. }
  158. if ($this->detectLoop($parent, $child)) {
  159. throw new InvalidCallException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected.");
  160. }
  161. if (isset($this->children[$parent->name][$child->name])) {
  162. throw new InvalidCallException("The item '{$parent->name}' already has a child '{$child->name}'.");
  163. }
  164. $this->children[$parent->name][$child->name] = $this->items[$child->name];
  165. $this->saveItems();
  166. return true;
  167. }
  168. /**
  169. * Checks whether there is a loop in the authorization item hierarchy.
  170. *
  171. * @param Item $parent parent item
  172. * @param Item $child the child item that is to be added to the hierarchy
  173. * @return boolean whether a loop exists
  174. */
  175. protected function detectLoop($parent, $child)
  176. {
  177. if ($child->name === $parent->name) {
  178. return true;
  179. }
  180. if (!isset($this->children[$child->name], $this->items[$parent->name])) {
  181. return false;
  182. }
  183. foreach ($this->children[$child->name] as $grandchild) {
  184. /* @var $grandchild Item */
  185. if ($this->detectLoop($parent, $grandchild)) {
  186. return true;
  187. }
  188. }
  189. return false;
  190. }
  191. /**
  192. * @inheritdoc
  193. */
  194. public function removeChild($parent, $child)
  195. {
  196. if (isset($this->children[$parent->name][$child->name])) {
  197. unset($this->children[$parent->name][$child->name]);
  198. $this->saveItems();
  199. return true;
  200. } else {
  201. return false;
  202. }
  203. }
  204. /**
  205. * @inheritdoc
  206. */
  207. public function removeChildren($parent)
  208. {
  209. if (isset($this->children[$parent->name])) {
  210. unset($this->children[$parent->name]);
  211. $this->saveItems();
  212. return true;
  213. } else {
  214. return false;
  215. }
  216. }
  217. /**
  218. * @inheritdoc
  219. */
  220. public function hasChild($parent, $child)
  221. {
  222. return isset($this->children[$parent->name][$child->name]);
  223. }
  224. /**
  225. * @inheritdoc
  226. */
  227. public function assign($role, $userId)
  228. {
  229. if (!isset($this->items[$role->name])) {
  230. throw new InvalidParamException("Unknown role '{$role->name}'.");
  231. } elseif (isset($this->assignments[$userId][$role->name])) {
  232. throw new InvalidParamException("Authorization item '{$role->name}' has already been assigned to user '$userId'.");
  233. } else {
  234. $this->assignments[$userId][$role->name] = new Assignment([
  235. 'userId' => $userId,
  236. 'roleName' => $role->name,
  237. 'createdAt' => time(),
  238. ]);
  239. $this->saveAssignments();
  240. return $this->assignments[$userId][$role->name];
  241. }
  242. }
  243. /**
  244. * @inheritdoc
  245. */
  246. public function revoke($role, $userId)
  247. {
  248. if (isset($this->assignments[$userId][$role->name])) {
  249. unset($this->assignments[$userId][$role->name]);
  250. $this->saveAssignments();
  251. return true;
  252. } else {
  253. return false;
  254. }
  255. }
  256. /**
  257. * @inheritdoc
  258. */
  259. public function revokeAll($userId)
  260. {
  261. if (isset($this->assignments[$userId]) && is_array($this->assignments[$userId])) {
  262. foreach ($this->assignments[$userId] as $itemName => $value) {
  263. unset($this->assignments[$userId][$itemName]);
  264. }
  265. $this->saveAssignments();
  266. return true;
  267. } else {
  268. return false;
  269. }
  270. }
  271. /**
  272. * @inheritdoc
  273. */
  274. public function getAssignment($roleName, $userId)
  275. {
  276. return isset($this->assignments[$userId][$roleName]) ? $this->assignments[$userId][$roleName] : null;
  277. }
  278. /**
  279. * @inheritdoc
  280. */
  281. public function getItems($type)
  282. {
  283. $items = [];
  284. foreach ($this->items as $name => $item) {
  285. /* @var $item Item */
  286. if ($item->type == $type) {
  287. $items[$name] = $item;
  288. }
  289. }
  290. return $items;
  291. }
  292. /**
  293. * @inheritdoc
  294. */
  295. public function removeItem($item)
  296. {
  297. if (isset($this->items[$item->name])) {
  298. foreach ($this->children as &$children) {
  299. unset($children[$item->name]);
  300. }
  301. foreach ($this->assignments as &$assignments) {
  302. unset($assignments[$item->name]);
  303. }
  304. unset($this->items[$item->name]);
  305. $this->saveItems();
  306. $this->saveAssignments();
  307. return true;
  308. } else {
  309. return false;
  310. }
  311. }
  312. /**
  313. * @inheritdoc
  314. */
  315. public function getItem($name)
  316. {
  317. return isset($this->items[$name]) ? $this->items[$name] : null;
  318. }
  319. /**
  320. * @inheritdoc
  321. */
  322. public function updateRule($name, $rule)
  323. {
  324. if ($rule->name !== $name) {
  325. unset($this->rules[$name]);
  326. }
  327. $this->rules[$rule->name] = $rule;
  328. $this->saveRules();
  329. return true;
  330. }
  331. /**
  332. * @inheritdoc
  333. */
  334. public function getRule($name)
  335. {
  336. return isset($this->rules[$name]) ? $this->rules[$name] : null;
  337. }
  338. /**
  339. * @inheritdoc
  340. */
  341. public function getRules()
  342. {
  343. return $this->rules;
  344. }
  345. /**
  346. * @inheritdoc
  347. */
  348. public function getRolesByUser($userId)
  349. {
  350. $roles = [];
  351. foreach ($this->getAssignments($userId) as $name => $assignment) {
  352. $role = $this->items[$assignment->roleName];
  353. if ($role->type === Item::TYPE_ROLE) {
  354. $roles[$name] = $role;
  355. }
  356. }
  357. return $roles;
  358. }
  359. /**
  360. * @inheritdoc
  361. */
  362. public function getPermissionsByRole($roleName)
  363. {
  364. $result = [];
  365. $this->getChildrenRecursive($roleName, $result);
  366. if (empty($result)) {
  367. return [];
  368. }
  369. $permissions = [];
  370. foreach (array_keys($result) as $itemName) {
  371. if (isset($this->items[$itemName]) && $this->items[$itemName] instanceof Permission) {
  372. $permissions[$itemName] = $this->items[$itemName];
  373. }
  374. }
  375. return $permissions;
  376. }
  377. /**
  378. * Recursively finds all children and grand children of the specified item.
  379. *
  380. * @param string $name the name of the item whose children are to be looked for.
  381. * @param array $result the children and grand children (in array keys)
  382. */
  383. protected function getChildrenRecursive($name, &$result)
  384. {
  385. if (isset($this->children[$name])) {
  386. foreach ($this->children[$name] as $child) {
  387. $result[$child->name] = true;
  388. $this->getChildrenRecursive($child->name, $result);
  389. }
  390. }
  391. }
  392. /**
  393. * @inheritdoc
  394. */
  395. public function getPermissionsByUser($userId)
  396. {
  397. $directPermission = $this->getDirectPermissionsByUser($userId);
  398. $inheritedPermission = $this->getInheritedPermissionsByUser($userId);
  399. return array_merge($directPermission, $inheritedPermission);
  400. }
  401. /**
  402. * Returns all permissions that are directly assigned to user.
  403. * @param string|integer $userId the user ID (see [[\yii\web\User::id]])
  404. * @return Permission[] all direct permissions that the user has. The array is indexed by the permission names.
  405. * @since 2.0.7
  406. */
  407. protected function getDirectPermissionsByUser($userId)
  408. {
  409. $permissions = [];
  410. foreach ($this->getAssignments($userId) as $name => $assignment) {
  411. $permission = $this->items[$assignment->roleName];
  412. if ($permission->type === Item::TYPE_PERMISSION) {
  413. $permissions[$name] = $permission;
  414. }
  415. }
  416. return $permissions;
  417. }
  418. /**
  419. * Returns all permissions that the user inherits from the roles assigned to him.
  420. * @param string|integer $userId the user ID (see [[\yii\web\User::id]])
  421. * @return Permission[] all inherited permissions that the user has. The array is indexed by the permission names.
  422. * @since 2.0.7
  423. */
  424. protected function getInheritedPermissionsByUser($userId)
  425. {
  426. $assignments = $this->getAssignments($userId);
  427. $result = [];
  428. foreach (array_keys($assignments) as $roleName) {
  429. $this->getChildrenRecursive($roleName, $result);
  430. }
  431. if (empty($result)) {
  432. return [];
  433. }
  434. $permissions = [];
  435. foreach (array_keys($result) as $itemName) {
  436. if (isset($this->items[$itemName]) && $this->items[$itemName] instanceof Permission) {
  437. $permissions[$itemName] = $this->items[$itemName];
  438. }
  439. }
  440. return $permissions;
  441. }
  442. /**
  443. * @inheritdoc
  444. */
  445. public function getChildren($name)
  446. {
  447. return isset($this->children[$name]) ? $this->children[$name] : [];
  448. }
  449. /**
  450. * @inheritdoc
  451. */
  452. public function removeAll()
  453. {
  454. $this->children = [];
  455. $this->items = [];
  456. $this->assignments = [];
  457. $this->rules = [];
  458. $this->save();
  459. }
  460. /**
  461. * @inheritdoc
  462. */
  463. public function removeAllPermissions()
  464. {
  465. $this->removeAllItems(Item::TYPE_PERMISSION);
  466. }
  467. /**
  468. * @inheritdoc
  469. */
  470. public function removeAllRoles()
  471. {
  472. $this->removeAllItems(Item::TYPE_ROLE);
  473. }
  474. /**
  475. * Removes all auth items of the specified type.
  476. * @param integer $type the auth item type (either Item::TYPE_PERMISSION or Item::TYPE_ROLE)
  477. */
  478. protected function removeAllItems($type)
  479. {
  480. $names = [];
  481. foreach ($this->items as $name => $item) {
  482. if ($item->type == $type) {
  483. unset($this->items[$name]);
  484. $names[$name] = true;
  485. }
  486. }
  487. if (empty($names)) {
  488. return;
  489. }
  490. foreach ($this->assignments as $i => $assignments) {
  491. foreach ($assignments as $n => $assignment) {
  492. if (isset($names[$assignment->roleName])) {
  493. unset($this->assignments[$i][$n]);
  494. }
  495. }
  496. }
  497. foreach ($this->children as $name => $children) {
  498. if (isset($names[$name])) {
  499. unset($this->children[$name]);
  500. } else {
  501. foreach ($children as $childName => $item) {
  502. if (isset($names[$childName])) {
  503. unset($children[$childName]);
  504. }
  505. }
  506. $this->children[$name] = $children;
  507. }
  508. }
  509. $this->saveItems();
  510. }
  511. /**
  512. * @inheritdoc
  513. */
  514. public function removeAllRules()
  515. {
  516. foreach ($this->items as $item) {
  517. $item->ruleName = null;
  518. }
  519. $this->rules = [];
  520. $this->saveRules();
  521. }
  522. /**
  523. * @inheritdoc
  524. */
  525. public function removeAllAssignments()
  526. {
  527. $this->assignments = [];
  528. $this->saveAssignments();
  529. }
  530. /**
  531. * @inheritdoc
  532. */
  533. protected function removeRule($rule)
  534. {
  535. if (isset($this->rules[$rule->name])) {
  536. unset($this->rules[$rule->name]);
  537. foreach ($this->items as $item) {
  538. if ($item->ruleName === $rule->name) {
  539. $item->ruleName = null;
  540. }
  541. }
  542. $this->saveRules();
  543. return true;
  544. } else {
  545. return false;
  546. }
  547. }
  548. /**
  549. * @inheritdoc
  550. */
  551. protected function addRule($rule)
  552. {
  553. $this->rules[$rule->name] = $rule;
  554. $this->saveRules();
  555. return true;
  556. }
  557. /**
  558. * @inheritdoc
  559. */
  560. protected function updateItem($name, $item)
  561. {
  562. if ($name !== $item->name) {
  563. if (isset($this->items[$item->name])) {
  564. throw new InvalidParamException("Unable to change the item name. The name '{$item->name}' is already used by another item.");
  565. } else {
  566. // Remove old item in case of renaming
  567. unset($this->items[$name]);
  568. if (isset($this->children[$name])) {
  569. $this->children[$item->name] = $this->children[$name];
  570. unset($this->children[$name]);
  571. }
  572. foreach ($this->children as &$children) {
  573. if (isset($children[$name])) {
  574. $children[$item->name] = $children[$name];
  575. unset($children[$name]);
  576. }
  577. }
  578. foreach ($this->assignments as &$assignments) {
  579. if (isset($assignments[$name])) {
  580. $assignments[$item->name] = $assignments[$name];
  581. $assignments[$item->name]->roleName = $item->name;
  582. unset($assignments[$name]);
  583. }
  584. }
  585. $this->saveAssignments();
  586. }
  587. }
  588. $this->items[$item->name] = $item;
  589. $this->saveItems();
  590. return true;
  591. }
  592. /**
  593. * @inheritdoc
  594. */
  595. protected function addItem($item)
  596. {
  597. $time = time();
  598. if ($item->createdAt === null) {
  599. $item->createdAt = $time;
  600. }
  601. if ($item->updatedAt === null) {
  602. $item->updatedAt = $time;
  603. }
  604. $this->items[$item->name] = $item;
  605. $this->saveItems();
  606. return true;
  607. }
  608. /**
  609. * Loads authorization data from persistent storage.
  610. */
  611. protected function load()
  612. {
  613. $this->children = [];
  614. $this->rules = [];
  615. $this->assignments = [];
  616. $this->items = [];
  617. $items = $this->loadFromFile($this->itemFile);
  618. $itemsMtime = @filemtime($this->itemFile);
  619. $assignments = $this->loadFromFile($this->assignmentFile);
  620. $assignmentsMtime = @filemtime($this->assignmentFile);
  621. $rules = $this->loadFromFile($this->ruleFile);
  622. foreach ($items as $name => $item) {
  623. $class = $item['type'] == Item::TYPE_PERMISSION ? Permission::className() : Role::className();
  624. $this->items[$name] = new $class([
  625. 'name' => $name,
  626. 'description' => isset($item['description']) ? $item['description'] : null,
  627. 'ruleName' => isset($item['ruleName']) ? $item['ruleName'] : null,
  628. 'data' => isset($item['data']) ? $item['data'] : null,
  629. 'createdAt' => $itemsMtime,
  630. 'updatedAt' => $itemsMtime,
  631. ]);
  632. }
  633. foreach ($items as $name => $item) {
  634. if (isset($item['children'])) {
  635. foreach ($item['children'] as $childName) {
  636. if (isset($this->items[$childName])) {
  637. $this->children[$name][$childName] = $this->items[$childName];
  638. }
  639. }
  640. }
  641. }
  642. foreach ($assignments as $userId => $roles) {
  643. foreach ($roles as $role) {
  644. $this->assignments[$userId][$role] = new Assignment([
  645. 'userId' => $userId,
  646. 'roleName' => $role,
  647. 'createdAt' => $assignmentsMtime,
  648. ]);
  649. }
  650. }
  651. foreach ($rules as $name => $ruleData) {
  652. $this->rules[$name] = unserialize($ruleData);
  653. }
  654. }
  655. /**
  656. * Saves authorization data into persistent storage.
  657. */
  658. protected function save()
  659. {
  660. $this->saveItems();
  661. $this->saveAssignments();
  662. $this->saveRules();
  663. }
  664. /**
  665. * Loads the authorization data from a PHP script file.
  666. *
  667. * @param string $file the file path.
  668. * @return array the authorization data
  669. * @see saveToFile()
  670. */
  671. protected function loadFromFile($file)
  672. {
  673. if (is_file($file)) {
  674. return require($file);
  675. } else {
  676. return [];
  677. }
  678. }
  679. /**
  680. * Saves the authorization data to a PHP script file.
  681. *
  682. * @param array $data the authorization data
  683. * @param string $file the file path.
  684. * @see loadFromFile()
  685. */
  686. protected function saveToFile($data, $file)
  687. {
  688. file_put_contents($file, "<?php\nreturn " . VarDumper::export($data) . ";\n", LOCK_EX);
  689. $this->invalidateScriptCache($file);
  690. }
  691. /**
  692. * Invalidates precompiled script cache (such as OPCache or APC) for the given file.
  693. * @param string $file the file path.
  694. * @since 2.0.9
  695. */
  696. protected function invalidateScriptCache($file)
  697. {
  698. if (function_exists('opcache_invalidate')) {
  699. opcache_invalidate($file, true);
  700. }
  701. if (function_exists('apc_delete_file')) {
  702. @apc_delete_file($file);
  703. }
  704. }
  705. /**
  706. * Saves items data into persistent storage.
  707. */
  708. protected function saveItems()
  709. {
  710. $items = [];
  711. foreach ($this->items as $name => $item) {
  712. /* @var $item Item */
  713. $items[$name] = array_filter(
  714. [
  715. 'type' => $item->type,
  716. 'description' => $item->description,
  717. 'ruleName' => $item->ruleName,
  718. 'data' => $item->data,
  719. ]
  720. );
  721. if (isset($this->children[$name])) {
  722. foreach ($this->children[$name] as $child) {
  723. /* @var $child Item */
  724. $items[$name]['children'][] = $child->name;
  725. }
  726. }
  727. }
  728. $this->saveToFile($items, $this->itemFile);
  729. }
  730. /**
  731. * Saves assignments data into persistent storage.
  732. */
  733. protected function saveAssignments()
  734. {
  735. $assignmentData = [];
  736. foreach ($this->assignments as $userId => $assignments) {
  737. foreach ($assignments as $name => $assignment) {
  738. /* @var $assignment Assignment */
  739. $assignmentData[$userId][] = $assignment->roleName;
  740. }
  741. }
  742. $this->saveToFile($assignmentData, $this->assignmentFile);
  743. }
  744. /**
  745. * Saves rules data into persistent storage.
  746. */
  747. protected function saveRules()
  748. {
  749. $rules = [];
  750. foreach ($this->rules as $name => $rule) {
  751. $rules[$name] = serialize($rule);
  752. }
  753. $this->saveToFile($rules, $this->ruleFile);
  754. }
  755. /**
  756. * @inheritdoc
  757. * @since 2.0.7
  758. */
  759. public function getUserIdsByRole($roleName)
  760. {
  761. $result = [];
  762. foreach ($this->assignments as $userID => $assignments) {
  763. foreach ($assignments as $userAssignment) {
  764. if ($userAssignment->roleName === $roleName && $userAssignment->userId == $userID) {
  765. $result[] = (string)$userID;
  766. }
  767. }
  768. }
  769. return $result;
  770. }
  771. }