plugin.class.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. /**
  4. * Class Plugin
  5. * Base class for plugins
  6. *
  7. * This class has to be extended by every plugin. It defines basic methods
  8. * to install/uninstall and get information about a plugin
  9. *
  10. * @author Julio Montoya <gugli100@gmail.com>
  11. * @author Yannick Warnier <ywarnier@beeznest.org>
  12. * @author Laurent Opprecht <laurent@opprecht.info>
  13. * @copyright 2012 University of Geneva
  14. * @license GNU General Public License - http://www.gnu.org/copyleft/gpl.html
  15. *
  16. */
  17. class Plugin
  18. {
  19. protected $version = '';
  20. protected $author = '';
  21. protected $fields = array();
  22. private $settings = null;
  23. // Translation strings.
  24. private $strings = null;
  25. public $isCoursePlugin = false;
  26. public $isMailPlugin = false;
  27. /**
  28. * When creating a new course, these settings are added to the course, in
  29. * the course_info/infocours.php
  30. * To show the plugin course icons you need to add these icons:
  31. * main/img/icons/22/plugin_name.png
  32. * main/img/icons/64/plugin_name.png
  33. * main/img/icons/64/plugin_name_na.png
  34. * @example
  35. * $course_settings = array(
  36. array('name' => 'big_blue_button_welcome_message', 'type' => 'text'),
  37. array('name' => 'big_blue_button_record_and_store', 'type' => 'checkbox')
  38. );
  39. */
  40. public $course_settings = array();
  41. /**
  42. * This indicates whether changing the setting should execute the callback
  43. * function.
  44. */
  45. public $course_settings_callback = false;
  46. /**
  47. * Default constructor for the plugin class. By default, it only sets
  48. * a few attributes of the object
  49. * @param string $version of this plugin
  50. * @param string $author of this plugin
  51. * @param array $settings settings to be proposed to configure the plugin
  52. */
  53. protected function __construct($version, $author, $settings = array())
  54. {
  55. $this->version = $version;
  56. $this->author = $author;
  57. $this->fields = $settings;
  58. global $language_files;
  59. $language_files[] = 'plugin_' . $this->get_name();
  60. }
  61. /**
  62. * Gets an array of information about this plugin (name, version, ...)
  63. * @return array Array of information elements about this plugin
  64. */
  65. public function get_info()
  66. {
  67. $result = array();
  68. $result['title'] = $this->get_title();
  69. $result['comment'] = $this->get_comment();
  70. $result['version'] = $this->get_version();
  71. $result['author'] = $this->get_author();
  72. $result['plugin_class'] = get_class($this);
  73. $result['is_course_plugin'] = $this->isCoursePlugin;
  74. $result['is_mail_plugin'] = $this->isMailPlugin;
  75. if ($form = $this->get_settings_form()) {
  76. $result['settings_form'] = $form;
  77. foreach ($this->fields as $name => $type) {
  78. $value = $this->get($name);
  79. if (is_array($type)) {
  80. $value = $type['options'];
  81. }
  82. $result[$name] = $value;
  83. }
  84. }
  85. return $result;
  86. }
  87. /**
  88. * Returns the "system" name of the plugin in lowercase letters
  89. * @return string
  90. */
  91. public function get_name()
  92. {
  93. $result = get_class($this);
  94. $result = str_replace('Plugin', '', $result);
  95. $result = strtolower($result);
  96. return $result;
  97. }
  98. /**
  99. * Returns the title of the plugin
  100. * @return string
  101. */
  102. public function get_title()
  103. {
  104. return $this->get_lang('plugin_title');
  105. }
  106. /**
  107. * Returns the description of the plugin
  108. * @return string
  109. */
  110. public function get_comment()
  111. {
  112. return $this->get_lang('plugin_comment');
  113. }
  114. /**
  115. * Returns the version of the plugin
  116. * @return string
  117. */
  118. public function get_version()
  119. {
  120. return $this->version;
  121. }
  122. /**
  123. * Returns the author of the plugin
  124. * @return string
  125. */
  126. public function get_author()
  127. {
  128. return $this->author;
  129. }
  130. /**
  131. * Returns the contents of the CSS defined by the plugin
  132. * @return array
  133. */
  134. public function get_css()
  135. {
  136. $name = $this->get_name();
  137. $path = api_get_path(SYS_PLUGIN_PATH)."$name/resources/$name.css";
  138. if (!is_readable($path)) {
  139. return '';
  140. }
  141. $css = array();
  142. $css[] = file_get_contents($path);
  143. $result = implode($css);
  144. return $result;
  145. }
  146. /**
  147. * Returns an HTML form (generated by FormValidator) of the plugin settings
  148. * @return string FormValidator-generated form
  149. */
  150. public function get_settings_form()
  151. {
  152. $result = new FormValidator($this->get_name());
  153. $defaults = array();
  154. $checkboxGroup = array();
  155. $checkboxCollection = array();
  156. if ($checkboxNames = array_keys($this->fields, 'checkbox')) {
  157. $pluginInfoCollection = api_get_settings('Plugins');
  158. foreach ($pluginInfoCollection as $pluginInfo) {
  159. if (array_search($pluginInfo['title'], $checkboxNames) !== false) {
  160. $checkboxCollection[$pluginInfo['title']] = $pluginInfo;
  161. }
  162. }
  163. }
  164. foreach ($this->fields as $name => $type) {
  165. $options = null;
  166. if (is_array($type) && isset($type['type']) && $type['type'] === "select") {
  167. $options = $type['options'];
  168. $type = $type['type'];
  169. }
  170. $value = $this->get($name);
  171. $defaults[$name] = $value;
  172. $type = isset($type) ? $type : 'text';
  173. $help = null;
  174. if ($this->get_lang_plugin_exists($name.'_help')) {
  175. $help = $this->get_lang($name.'_help');
  176. if ($name === "show_main_menu_tab") {
  177. $pluginName = strtolower(str_replace('Plugin', '', get_class($this)));
  178. $pluginUrl = api_get_path(WEB_PATH)."plugin/$pluginName/index.php";
  179. $pluginUrl = "<a href=$pluginUrl>$pluginUrl</a>";
  180. $help = sprintf($help, $pluginUrl);
  181. }
  182. }
  183. switch ($type) {
  184. case 'html':
  185. $result->addElement('html', $this->get_lang($name));
  186. break;
  187. case 'wysiwyg':
  188. $result->addHtmlEditor($name, $this->get_lang($name), false);
  189. break;
  190. case 'text':
  191. $result->addElement($type, $name, array($this->get_lang($name), $help));
  192. break;
  193. case 'boolean':
  194. $group = array();
  195. $group[] = $result->createElement('radio', $name, '', get_lang('Yes'), 'true');
  196. $group[] = $result->createElement('radio', $name, '', get_lang('No'), 'false');
  197. $result->addGroup($group, null, array($this->get_lang($name), $help));
  198. break;
  199. case 'checkbox':
  200. $selectedValue = null;
  201. if (isset($checkboxCollection[$name])) {
  202. if ($checkboxCollection[$name]['selected_value'] === 'true') {
  203. $selectedValue = 'checked';
  204. }
  205. }
  206. $element = $result->createElement(
  207. $type,
  208. $name,
  209. '',
  210. $this->get_lang($name),
  211. $selectedValue
  212. );
  213. $element->_attributes['value'] = 'true';
  214. $checkboxGroup[] = $element;
  215. break;
  216. case 'select':
  217. $result->addElement(
  218. $type,
  219. $name,
  220. array($this->get_lang($name), $help),
  221. $options
  222. );
  223. break;
  224. }
  225. }
  226. if (!empty($checkboxGroup)) {
  227. $result->addGroup($checkboxGroup, null, array($this->get_lang('sms_types'), $help));
  228. }
  229. $result->setDefaults($defaults);
  230. $result->addButtonSave($this->get_lang('Save'), 'submit_button');
  231. return $result;
  232. }
  233. /**
  234. * Returns the value of a given plugin global setting
  235. * @param string $name of the plugin
  236. *
  237. * @return string Value of the plugin
  238. */
  239. public function get($name)
  240. {
  241. $settings = $this->get_settings();
  242. foreach ($settings as $setting) {
  243. if ($setting['variable'] == ($this->get_name() . '_' . $name)) {
  244. return $setting['selected_value'];
  245. }
  246. }
  247. return false;
  248. }
  249. /**
  250. * Returns an array with the global settings for this plugin
  251. * @return array Plugin settings as an array
  252. */
  253. public function get_settings()
  254. {
  255. if (is_null($this->settings)) {
  256. $settings = api_get_settings_params(
  257. array(
  258. "subkey = ? AND category = ? AND type = ? " => array($this->get_name(), 'Plugins', 'setting')
  259. )
  260. );
  261. $this->settings = $settings;
  262. }
  263. return $this->settings;
  264. }
  265. /**
  266. * Tells whether language variables are defined for this plugin or not
  267. * @param string $name System name of the plugin
  268. *
  269. * @return bool True if the plugin has language variables defined, false otherwise
  270. */
  271. public function get_lang_plugin_exists($name)
  272. {
  273. return isset($this->strings[$name]);
  274. }
  275. /**
  276. * Hook for the get_lang() function to check for plugin-defined language terms
  277. * @param string $name of the language variable we are looking for
  278. *
  279. * @return string The translated language term of the plugin
  280. */
  281. public function get_lang($name)
  282. {
  283. // Check whether the language strings for the plugin have already been
  284. // loaded. If so, no need to load them again.
  285. if (is_null($this->strings)) {
  286. global $language_interface;
  287. $root = api_get_path(SYS_PLUGIN_PATH);
  288. $plugin_name = $this->get_name();
  289. $interfaceLanguageId = api_get_language_id($language_interface);
  290. $interfaceLanguageInfo = api_get_language_info($interfaceLanguageId);
  291. $languageParentId = intval($interfaceLanguageInfo['parent_id']);
  292. //1. Loading english if exists
  293. $english_path = $root.$plugin_name."/lang/english.php";
  294. if (is_readable($english_path)) {
  295. $strings = array();
  296. include $english_path;
  297. $this->strings = $strings;
  298. }
  299. $path = $root.$plugin_name."/lang/$language_interface.php";
  300. //2. Loading the system language
  301. if (is_readable($path)) {
  302. include $path;
  303. if (!empty($strings)) {
  304. foreach ($strings as $key => $string) {
  305. $this->strings[$key] = $string;
  306. }
  307. }
  308. } elseif ($languageParentId > 0) {
  309. $languageParentInfo = api_get_language_info($languageParentId);
  310. $languageParentFolder = $languageParentInfo['dokeos_folder'];
  311. $parentPath = "{$root}{$plugin_name}/lang/{$languageParentFolder}.php";
  312. if (is_readable($parentPath)) {
  313. include $parentPath;
  314. if (!empty($strings)) {
  315. foreach ($strings as $key => $string) {
  316. $this->strings[$key] = $string;
  317. }
  318. }
  319. }
  320. }
  321. }
  322. if (isset($this->strings[$name])) {
  323. return $this->strings[$name];
  324. }
  325. return get_lang($name);
  326. }
  327. /**
  328. * Caller for the install_course_fields() function
  329. * @param int $courseId
  330. *
  331. * @param boolean $addToolLink Whether to add a tool link on the course homepage
  332. *
  333. * @return void
  334. */
  335. public function course_install($courseId, $addToolLink = true)
  336. {
  337. $this->install_course_fields($courseId, $addToolLink);
  338. }
  339. /**
  340. * Add course settings and, if not asked otherwise, add a tool link on the course homepage
  341. * @param int $courseId Course integer ID
  342. * @param boolean $add_tool_link Whether to add a tool link or not
  343. * (some tools might just offer a configuration section and act on the backend)
  344. *
  345. * @return boolean False on error, null otherwise
  346. */
  347. public function install_course_fields($courseId, $add_tool_link = true)
  348. {
  349. $plugin_name = $this->get_name();
  350. $t_course = Database::get_course_table(TABLE_COURSE_SETTING);
  351. $courseId = intval($courseId);
  352. if (empty($courseId)) {
  353. return false;
  354. }
  355. // Adding course settings.
  356. if (!empty($this->course_settings)) {
  357. foreach ($this->course_settings as $setting) {
  358. $variable = $setting['name'];
  359. $value ='';
  360. if (isset($setting['init_value'])) {
  361. $value = ($setting['init_value']);
  362. }
  363. $type = 'textfield';
  364. if (isset($setting['type'])) {
  365. $type = $setting['type'];
  366. }
  367. if (isset($setting['group'])) {
  368. $group = $setting['group'];
  369. $sql = "SELECT value
  370. FROM $t_course
  371. WHERE
  372. c_id = $courseId AND
  373. variable = '".Database::escape_string($group)."' AND
  374. subkey = '".Database::escape_string($variable)."'
  375. ";
  376. $result = Database::query($sql);
  377. if (!Database::num_rows($result)) {
  378. $params = [
  379. 'c_id' => $courseId,
  380. 'variable' => $group,
  381. 'subkey' => $variable,
  382. 'value' => $value,
  383. 'category' => 'plugins',
  384. 'type' => $type
  385. ];
  386. Database::insert($t_course, $params);
  387. }
  388. } else {
  389. $sql = "SELECT value FROM $t_course
  390. WHERE c_id = $courseId AND variable = '$variable' ";
  391. $result = Database::query($sql);
  392. if (!Database::num_rows($result)) {
  393. $params = [
  394. 'c_id' => $courseId,
  395. 'variable' => $variable,
  396. 'subkey' => $plugin_name,
  397. 'value' => $value,
  398. 'category' => 'plugins',
  399. 'type' => $type
  400. ];
  401. Database::insert($t_course, $params);
  402. }
  403. }
  404. }
  405. }
  406. // Stop here if we don't want a tool link on the course homepage
  407. if (!$add_tool_link) {
  408. return true;
  409. }
  410. //Add an icon in the table tool list
  411. $table = Database::get_course_table(TABLE_TOOL_LIST);
  412. $sql = "SELECT name FROM $table
  413. WHERE c_id = $courseId AND name = '$plugin_name' ";
  414. $result = Database::query($sql);
  415. if (!Database::num_rows($result)) {
  416. $tool_link = "$plugin_name/start.php";
  417. //$visibility = AddCourse::string2binary(api_get_setting('course_create_active_tools', $plugin_name));
  418. $cToolId = AddCourse::generateToolId($courseId);
  419. Database::insert(
  420. $table,
  421. [
  422. 'id' => $cToolId,
  423. 'c_id' => $courseId,
  424. 'name' => $plugin_name,
  425. 'link' => $tool_link,
  426. 'image' => "$plugin_name.png",
  427. 'visibility' => 1,
  428. 'admin' => 0,
  429. 'address' => 'squaregrey.gif',
  430. 'added_tool' => 'NO',
  431. 'target' => '_self',
  432. 'category' => 'plugin',
  433. 'session_id' => 0
  434. ]
  435. );
  436. }
  437. }
  438. /**
  439. * Delete the fields added to the course settings page and the link to the
  440. * tool on the course's homepage
  441. * @param int $courseId
  442. *
  443. * @return void
  444. */
  445. public function uninstall_course_fields($courseId)
  446. {
  447. $courseId = intval($courseId);
  448. if (empty($courseId)) {
  449. return false;
  450. }
  451. $plugin_name = $this->get_name();
  452. $t_course = Database::get_course_table(TABLE_COURSE_SETTING);
  453. $t_tool = Database::get_course_table(TABLE_TOOL_LIST);
  454. if (!empty($this->course_settings)) {
  455. foreach ($this->course_settings as $setting) {
  456. $variable = Database::escape_string($setting['name']);
  457. if (!empty($setting['group'])) {
  458. $variable = Database::escape_string($setting['group']);
  459. }
  460. if (empty($variable)) {
  461. continue;
  462. }
  463. $sql = "DELETE FROM $t_course
  464. WHERE c_id = $courseId AND variable = '$variable'";
  465. Database::query($sql);
  466. }
  467. }
  468. $plugin_name = Database::escape_string($plugin_name);
  469. $sql = "DELETE FROM $t_tool
  470. WHERE c_id = $courseId AND name = '$plugin_name'";
  471. Database::query($sql);
  472. }
  473. /**
  474. * Install the course fields and tool link of this plugin in all courses
  475. * @param boolean Whether we want to add a plugin link on the course homepage
  476. *
  477. * @return void
  478. */
  479. public function install_course_fields_in_all_courses($add_tool_link = true)
  480. {
  481. // Update existing courses to add plugin settings
  482. $t_courses = Database::get_main_table(TABLE_MAIN_COURSE);
  483. $sql = "SELECT id FROM $t_courses ORDER BY id";
  484. $res = Database::query($sql);
  485. while ($row = Database::fetch_assoc($res)) {
  486. $this->install_course_fields($row['id'], $add_tool_link);
  487. }
  488. }
  489. /**
  490. * Uninstall the plugin settings fields from all courses
  491. * @return void
  492. */
  493. public function uninstall_course_fields_in_all_courses()
  494. {
  495. // Update existing courses to add conference settings
  496. $t_courses = Database::get_main_table(TABLE_MAIN_COURSE);
  497. $sql = "SELECT id FROM $t_courses
  498. ORDER BY id";
  499. $res = Database::query($sql);
  500. while ($row = Database::fetch_assoc($res)) {
  501. $this->uninstall_course_fields($row['id']);
  502. }
  503. }
  504. /**
  505. * @return array
  506. */
  507. public function getCourseSettings()
  508. {
  509. $settings = array();
  510. if (is_array($this->course_settings)) {
  511. foreach ($this->course_settings as $item) {
  512. if (isset($item['group'])) {
  513. if (!in_array($item['group'], $settings)) {
  514. $settings[] = $item['group'];
  515. }
  516. } else {
  517. $settings[] = $item['name'];
  518. }
  519. }
  520. }
  521. return $settings;
  522. }
  523. /**
  524. * Method to be extended when changing the setting in the course
  525. * configuration should trigger the use of a callback method
  526. * @param array $values sent back from the course configuration script
  527. *
  528. * @return void
  529. */
  530. public function course_settings_updated($values = array())
  531. {
  532. }
  533. /**
  534. * Add a tab to platform
  535. * @param string $tabName
  536. * @param string $url
  537. *
  538. * @return boolean
  539. */
  540. public function addTab($tabName, $url)
  541. {
  542. $sql = "SELECT * FROM settings_current
  543. WHERE
  544. variable = 'show_tabs' AND
  545. subkey LIKE 'custom_tab_%'";
  546. $result = Database::query($sql);
  547. $customTabsNum = Database::num_rows($result);
  548. $tabNum = $customTabsNum + 1;
  549. //Avoid Tab Name Spaces
  550. $tabNameNoSpaces = preg_replace('/\s+/', '', $tabName);
  551. $subkeytext = "Tabs" . $tabNameNoSpaces;
  552. //Check if it is already added
  553. $checkCondition = array(
  554. 'where' =>
  555. array(
  556. "variable = 'show_tabs' AND subkeytext = ?" => array(
  557. $subkeytext
  558. )
  559. )
  560. );
  561. $checkDuplicate = Database::select('*', 'settings_current', $checkCondition);
  562. if (!empty($checkDuplicate)) {
  563. return false;
  564. }
  565. //End Check
  566. $subkey = 'custom_tab_' . $tabNum;
  567. $attributes = array(
  568. 'variable' => 'show_tabs',
  569. 'subkey' => $subkey,
  570. 'type' => 'checkbox',
  571. 'category' => 'Platform',
  572. 'selected_value' => 'true',
  573. 'title' => $tabName,
  574. 'comment' => $url,
  575. 'subkeytext' => $subkeytext,
  576. 'access_url' => 1,
  577. 'access_url_changeable' => 0,
  578. 'access_url_locked' => 0
  579. );
  580. $resp = Database::insert('settings_current', $attributes);
  581. //Save the id
  582. $settings = $this->get_settings();
  583. $setData = array (
  584. 'comment' => $subkey
  585. );
  586. $whereCondition = array(
  587. 'id = ?' => key($settings)
  588. );
  589. Database::update('settings_current', $setData, $whereCondition);
  590. return $resp;
  591. }
  592. /**
  593. * Delete a tab to chamilo's platform
  594. * @param string $key
  595. * @return boolean $resp Transaction response
  596. */
  597. public function deleteTab($key)
  598. {
  599. $t = Database::get_main_table(TABLE_MAIN_SETTINGS_CURRENT);
  600. $sql = "SELECT *
  601. FROM $t
  602. WHERE variable = 'show_tabs'
  603. AND subkey <> '$key'
  604. AND subkey like 'custom_tab_%'
  605. ";
  606. $resp = $result = Database::query($sql);
  607. $customTabsNum = Database::num_rows($result);
  608. if (!empty($key)) {
  609. $whereCondition = array(
  610. 'variable = ? AND subkey = ?' => array('show_tabs', $key)
  611. );
  612. $resp = Database::delete('settings_current', $whereCondition);
  613. //if there is more than one tab
  614. //re enumerate them
  615. if (!empty($customTabsNum) && $customTabsNum > 0) {
  616. $tabs = Database::store_result($result, 'ASSOC');
  617. $i = 1;
  618. foreach ($tabs as $row) {
  619. $attributes = array(
  620. 'subkey' => 'custom_tab_' . $i
  621. );
  622. $this->updateTab($row['subkey'], $attributes);
  623. $i++;
  624. }
  625. }
  626. }
  627. return $resp;
  628. }
  629. /**
  630. * Update the tabs attributes
  631. * @param string $key
  632. * @param array $attributes
  633. *
  634. * @return boolean
  635. */
  636. public function updateTab($key, $attributes)
  637. {
  638. $whereCondition = array(
  639. 'variable = ? AND subkey = ?' => array('show_tabs', $key)
  640. );
  641. $resp = Database::update('settings_current', $attributes, $whereCondition);
  642. return $resp;
  643. }
  644. /**
  645. * This method shows or hides plugin's tab
  646. * @param boolean $showTab Shows or hides the main menu plugin tab
  647. * @param string $filePath Plugin starter file path
  648. */
  649. public function manageTab($showTab, $filePath = 'index.php')
  650. {
  651. $langString = str_replace('Plugin', '', get_class($this));
  652. $pluginName = strtolower($langString);
  653. $pluginUrl = 'plugin/'.$pluginName.'/'.$filePath;
  654. if ($showTab === 'true') {
  655. $tabAdded = $this->addTab($langString, $pluginUrl);
  656. if ($tabAdded) {
  657. // The page must be refreshed to show the recently created tab
  658. echo "<script>location.href = '".Security::remove_XSS($_SERVER['REQUEST_URI'])."';</script>";
  659. }
  660. } else {
  661. $settingsCurrentTable = Database::get_main_table(TABLE_MAIN_SETTINGS_CURRENT);
  662. $conditions = array(
  663. 'where' => array(
  664. "variable = 'show_tabs' AND title = ? AND comment = ? " => array(
  665. $langString,
  666. $pluginUrl
  667. )
  668. )
  669. );
  670. $result = Database::select('subkey', $settingsCurrentTable, $conditions);
  671. if (!empty($result)) {
  672. $this->deleteTab($result[0]['subkey']);
  673. }
  674. }
  675. }
  676. }