Updated all the contrib modules to their latest versions.
[yaffs-website] / web / modules / contrib / linkchecker / src / Form / LinkCheckerAdminSettingsForm.php
1 <?php
2
3 namespace Drupal\linkchecker\Form;
4
5 use Drupal\Core\Form\ConfigFormBase;
6 use Drupal\Core\Form\FormStateInterface;
7 use Drupal\Core\Logger\RfcLogLevel;
8 use Drupal\Core\Url;
9 use Drupal\filter\FilterPluginCollection;
10 use Drupal\user\Entity\User;
11
12 /**
13  * Configure Linkchecker settings for this site.
14  */
15 class LinkCheckerAdminSettingsForm extends ConfigFormBase {
16
17   /**
18    * {@inheritdoc}
19    */
20   public function getFormId() {
21     return 'linkchecker_admin_settings';
22   }
23
24   /**
25    * {@inheritdoc}
26    */
27   protected function getEditableConfigNames() {
28     return ['linkchecker.settings'];
29   }
30
31   /**
32    * {@inheritdoc}
33    */
34   public function buildForm(array $form, FormStateInterface $form_state) {
35     $config = $this->config('linkchecker.settings');
36
37     $form['general'] = [
38       '#type' => 'details',
39       '#title' => $this->t('General settings'),
40       '#description' => $this->t('Configure the <a href=":url">content types</a> that should be scanned for broken links.', [':url' => Url::fromRoute('entity.node_type.collection')->toString()]),
41       '#open' => TRUE,
42     ];
43
44     $block_custom_dependencies = '<div class="admin-requirements">';
45     $block_custom_dependencies .= $this->t('Requires: @module-list',
46       [
47         '@module-list' => (\Drupal::moduleHandler()->moduleExists('block') ? $this->t('@module (<span class="admin-enabled">enabled</span>)', ['@module' => 'Block']) : $this->t('@module (<span class="admin-disabled">disabled</span>)', ['@module' => 'Block']))]);
48     $block_custom_dependencies .= '</div>';
49
50     $form['general']['linkchecker_scan_blocks'] = [
51       '#default_value' => $config->get('scan_blocks'),
52       '#type' => 'checkbox',
53       '#title' => $this->t('Scan blocks for links'),
54       '#description' => $this->t('Enable this checkbox if links in blocks should be checked.') . $block_custom_dependencies,
55       '#disabled' => !\Drupal::moduleHandler()->moduleExists('block'),
56     ];
57     $form['general']['linkchecker_check_links_types'] = [
58       '#type' => 'select',
59       '#title' => $this->t('What type of links should be checked?'),
60       '#description' => $this->t('A full qualified link (http://example.com/foo/bar) to a page is considered external, whereas an absolute (/foo/bar) or relative link (node/123) without a domain is considered internal.'),
61       '#default_value' => $config->get('check_links_types'),
62       '#options' => [
63         '0' => $this->t('Internal and external'),
64         '1' => $this->t('External only (http://example.com/foo/bar)'),
65         '2' => $this->t('Internal only (node/123)'),
66       ],
67     ];
68
69     $form['tag'] = [
70       '#type' => 'details',
71       '#title' => $this->t('Link extraction'),
72       '#open' => TRUE,
73     ];
74     $form['tag']['linkchecker_extract_from_a'] = [
75       '#default_value' => $config->get('extract.from_a'),
76       '#type' => 'checkbox',
77       '#title' => $this->t('Extract links in <code>&lt;a&gt;</code> and <code>&lt;area&gt;</code> tags'),
78       '#description' => $this->t('Enable this checkbox if normal hyperlinks should be extracted. The anchor element defines a hyperlink, the named target destination for a hyperlink, or both. The area element defines a hot-spot region on an image, and associates it with a hypertext link.'),
79     ];
80     $form['tag']['linkchecker_extract_from_audio'] = [
81       '#default_value' => $config->get('extract.from_audio'),
82       '#type' => 'checkbox',
83       '#title' => $this->t('Extract links in <code>&lt;audio&gt;</code> tags including their <code>&lt;source&gt;</code> and <code>&lt;track&gt;</code> tags'),
84       '#description' => $this->t('Enable this checkbox if links in audio tags should be extracted. The audio element is used to embed audio content.'),
85     ];
86     $form['tag']['linkchecker_extract_from_embed'] = [
87       '#default_value' => $config->get('extract.from_embed'),
88       '#type' => 'checkbox',
89       '#title' => $this->t('Extract links in <code>&lt;embed&gt;</code> tags'),
90       '#description' => $this->t('Enable this checkbox if links in embed tags should be extracted. This is an obsolete and non-standard element that was used for embedding plugins in past and should no longer used in modern websites.'),
91     ];
92     $form['tag']['linkchecker_extract_from_iframe'] = [
93       '#default_value' => $config->get('extract.from_iframe'),
94       '#type' => 'checkbox',
95       '#title' => $this->t('Extract links in <code>&lt;iframe&gt;</code> tags'),
96       '#description' => $this->t('Enable this checkbox if links in iframe tags should be extracted. The iframe element is used to embed another HTML page into a page.'),
97     ];
98     $form['tag']['linkchecker_extract_from_img'] = [
99       '#default_value' => $config->get('extract.from_img'),
100       '#type' => 'checkbox',
101       '#title' => $this->t('Extract links in <code>&lt;img&gt;</code> tags'),
102       '#description' => $this->t('Enable this checkbox if links in image tags should be extracted. The img element is used to add images to the content.'),
103     ];
104     $form['tag']['linkchecker_extract_from_object'] = [
105       '#default_value' => $config->get('extract.from_object'),
106       '#type' => 'checkbox',
107       '#title' => $this->t('Extract links in <code>&lt;object&gt;</code> and <code>&lt;param&gt;</code> tags'),
108       '#description' => $this->t('Enable this checkbox if multimedia and other links in object and their param tags should be extracted. The object tag is used for flash, java, quicktime and other applets.'),
109     ];
110     $form['tag']['linkchecker_extract_from_video'] = [
111       '#default_value' => $config->get('extract.from_video'),
112       '#type' => 'checkbox',
113       '#title' => $this->t('Extract links in <code>&lt;video&gt;</code> tags including their <code>&lt;source&gt;</code> and <code>&lt;track&gt;</code> tags'),
114       '#description' => $this->t('Enable this checkbox if links in video tags should be extracted. The video element is used to embed video content.'),
115     ];
116
117     // Get all filters available on the system.
118     $manager = \Drupal::service('plugin.manager.filter');
119     $bag = new FilterPluginCollection($manager, []);
120     $filter_info = $bag->getAll();
121     $filter_options = [];
122     $filter_descriptions = [];
123     foreach ($filter_info as $name => $filter) {
124       if (in_array($name, explode('|', LINKCHECKER_DEFAULT_FILTER_BLACKLIST))) {
125         $filter_options[$name] = $this->t('@title <span class="marker">(Recommended)</span>', ['@title' => $filter->getLabel()]);
126       }
127       else {
128         $filter_options[$name] = $filter->getLabel();
129       }
130       $filter_descriptions[$name] = [
131         '#description' => $filter->getDescription(),
132       ];
133     }
134     $form['tag']['linkchecker_filter_blacklist'] = [
135       '#type' => 'checkboxes',
136       '#title' => $this->t('Text formats disabled for link extraction'),
137       '#default_value' => $config->get('extract.filter_blacklist'),
138       '#options' => $filter_options,
139       '#description' => $this->t('If a filter has been enabled for an input format it runs first and afterwards the link extraction. This helps the link checker module to find all links normally created by custom filters (e.g. Markdown filter, Bbcode). All filters used as inline references (e.g. Weblink filter <code>[link: id]</code>) to other content and filters only wasting processing time (e.g. Line break converter) should be disabled. This setting does not have any effect on how content is shown on a page. This feature optimizes the internal link extraction process for link checker and prevents false alarms about broken links in content not having the real data of a link.'),
140     ];
141     $form['tag']['linkchecker_filter_blacklist'] = array_merge($form['tag']['linkchecker_filter_blacklist'], $filter_descriptions);
142
143     $count_lids_enabled = db_query("SELECT count(lid) FROM {linkchecker_link} WHERE status = :status", [':status' => 1])->fetchField();
144     $count_lids_disabled = db_query("SELECT count(lid) FROM {linkchecker_link} WHERE status = :status", [':status' => 0])->fetchField();
145
146     // httprl module does not exists yet for D8
147     /* $form['check'] = [
148       '#type' => 'details',
149       '#title' => $this->t('Check settings'),
150       '#description' => $this->t('For simultaneous link checks it is recommended to install the <a href=":httprl">HTTP Parallel Request & Threading Library</a>. This may be <strong>necessary</strong> on larger sites with very many links (30.000+), but will also improve overall link check duration on smaller sites. Currently the site has @count links (@count_enabled enabled / @count_disabled disabled).', [':httprl' => 'http://drupal.org/project/httprl', '@count' => $count_lids_enabled+$count_lids_disabled, '@count_enabled' => $count_lids_enabled, '@count_disabled' => $count_lids_disabled]),
151       '#open' => TRUE,
152     ];*/
153     $form['check']['linkchecker_check_library'] = [
154       '#type' => 'select',
155       '#title' => $this->t('Check library'),
156       '#description' => $this->t('Defines the library that is used for checking links.'),
157       '#default_value' => $config->get('check.library'),
158       '#options' => [
159         'core' => $this->t('Drupal core'),
160         // 'httprl' => $this->t('HTTP Parallel Request & Threading Library'),
161       ],
162     ];
163     $form['check']['linkchecker_check_connections_max'] = [
164       '#type' => 'select',
165       '#title' => $this->t('Number of simultaneous connections'),
166       '#description' => $this->t('Defines the maximum number of simultaneous connections that can be opened by the server. <em>HTTP Parallel Request & Threading Library</em> make sure that a single domain is not overloaded beyond RFC limits. For small hosting plans with very limited CPU and RAM it may be required to reduce the default limit.'),
167       '#default_value' => $config->get('check.connections_max'),
168       '#options' => array_combine([2, 4, 8, 16, 24, 32, 48, 64, 96, 128], [2, 4, 8, 16, 24, 32, 48, 64, 96, 128]),
169       '#states' => [
170         // Hide the setting when Drupal core check library is selected.
171         'invisible' => [
172           ':input[name="check_library"]' => ['value' => 'core'],
173         ],
174       ],
175     ];
176     $form['check']['linkchecker_check_useragent'] = [
177       '#type' => 'select',
178       '#title' => $this->t('User-Agent'),
179       '#description' => $this->t('Defines the user agent that will be used for checking links on remote sites. If someone blocks the standard Drupal user agent you can try with a more common browser.'),
180       '#default_value' => $config->get('check.useragent'),
181       '#options' => [
182         'Drupal (+http://drupal.org/)' => 'Drupal (+http://drupal.org/)',
183         'Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko' => 'Windows 8.1 (x64), Internet Explorer 11.0',
184         'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586' => 'Windows 10 (x64), Edge',
185         'Mozilla/5.0 (Windows NT 6.3; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0' => 'Windows 8.1 (x64), Mozilla Firefox 47.0',
186         'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0' => 'Windows 10 (x64), Mozilla Firefox 47.0',
187       ],
188     ];
189     $intervals = [86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 7776000];
190     $period = array_map([\Drupal::service('date.formatter'), 'formatInterval'], array_combine($intervals, $intervals));
191     $form['check']['linkchecker_check_interval'] = [
192       '#type' => 'select',
193       '#title' => $this->t('Check interval for links'),
194       '#description' => $this->t('This interval setting defines how often cron will re-check the status of links.'),
195       '#default_value' => $config->get('check.interval'),
196       '#options' => $period,
197     ];
198     $form['check']['linkchecker_disable_link_check_for_urls'] = [
199       '#default_value' => $config->get('check.disable_link_check_for_urls'),
200       '#type' => 'textarea',
201       '#title' => $this->t('Do not check the link status of links containing these URLs'),
202       '#description' => $this->t('By default this list contains the domain names reserved for use in documentation and not available for registration. See <a href=":rfc-2606">RFC 2606</a>, Section 3 for more information. URLs on this list are still extracted, but the link setting <em>Check link status</em> becomes automatically disabled to prevent false alarms. If you change this list you need to clear all link data and re-analyze your content. Otherwise this setting will only affect new links added after the configuration change.', [':rfc-2606' => 'http://www.rfc-editor.org/rfc/rfc2606.txt']),
203     ];
204     // @fixme: constants no longer exists.
205     $form['check']['linkchecker_logging_level'] = [
206       '#default_value' => $config->get('logging.level'),
207       '#type' => 'select',
208       '#title' => $this->t('Log level'),
209       '#description' => $this->t('Controls the severity of logging.'),
210       '#options' => [
211         RfcLogLevel::DEBUG => $this->t('Debug messages'),
212         RfcLogLevel::INFO => $this->t('All messages (default)'),
213         RfcLogLevel::NOTICE => $this->t('Notices and errors'),
214         RfcLogLevel::WARNING => $this->t('Warnings and errors'),
215         RfcLogLevel::ERROR => $this->t('Errors only'),
216       ],
217     ];
218
219     $form['error'] = [
220       '#type' => 'details',
221       '#title' => $this->t('Error handling'),
222       '#description' => $this->t('Defines error handling and custom actions to be executed if specific HTTP requests are failing.'),
223       '#open' => TRUE,
224     ];
225     $linkchecker_default_impersonate_account = User::load(1);
226     $form['error']['linkchecker_impersonate_account'] = [
227       '#type' => 'textfield',
228       '#title' => $this->t('Impersonate user account'),
229       '#description' => $this->t('If below error handling actions are executed they can be impersonated with a custom user account. By default this is user %name, but you are able to assign a custom user to allow easier identification of these automatic revision updates. Make sure you select a user with <em>full</em> permissions on your site or the user may not able to access and save all content.', ['%name' => $linkchecker_default_impersonate_account->getAccountName()]),
230       '#size' => 30,
231       '#maxlength' => 60,
232       '#autocomplete_path' => 'user/autocomplete',
233       '#default_value' => $config->get('error.impersonate_account'),
234     ];
235     $form['error']['linkchecker_action_status_code_301'] = [
236       '#title' => $this->t('Update permanently moved links'),
237       '#description' => $this->t('If enabled, outdated links in content providing a status <em>Moved Permanently</em> (status code 301) are automatically updated to the most recent URL. If used, it is recommended to use a value of <em>three</em> to make sure this is not only a temporarily change. This feature trust sites to provide a valid permanent redirect. A new content revision is automatically created on link updates if <em>create new revision</em> is enabled in the <a href=":content_types">content types</a> publishing options. It is recommended to create new revisions for all link checker enabled content types. Link updates are nevertheless always logged in <a href=":dblog">recent log entries</a>.', [':dblog' => Url::fromRoute('dblog.overview')->toString(), ':content_types' => Url::fromRoute('entity.node_type.collection')->toString()]),
238       '#type' => 'select',
239       '#default_value' => $config->get('error.action_status_code_301'),
240       '#options' => [
241         0 => $this->t('Disabled'),
242         1 => $this->t('After one failed check'),
243         2 => $this->t('After two failed checks'),
244         3 => $this->t('After three failed checks'),
245         5 => $this->t('After five failed checks'),
246         10 => $this->t('After ten failed checks'),
247       ],
248     ];
249     $form['error']['linkchecker_action_status_code_404'] = [
250       '#title' => $this->t('Unpublish content on file not found error'),
251       '#description' => $this->t('If enabled, content with one or more broken links (status code 404) will be unpublished and moved to moderation queue for review after the number of specified checks failed. If used, it is recommended to use a value of <em>three</em> to make sure this is not only a temporarily error.'),
252       '#type' => 'select',
253       '#default_value' => $config->get('error.action_status_code_404'),
254       '#options' => [
255         0 => $this->t('Disabled'),
256         1 => $this->t('After one file not found error'),
257         2 => $this->t('After two file not found errors'),
258         3 => $this->t('After three file not found errors'),
259         5 => $this->t('After five file not found errors'),
260         10 => $this->t('After ten file not found errors'),
261       ],
262     ];
263     $form['error']['linkchecker_ignore_response_codes'] = [
264       '#default_value' => $config->get('error.ignore_response_codes'),
265       '#type' => 'textarea',
266       '#title' => $this->t("Don't treat these response codes as errors"),
267       '#description' => $this->t('One HTTP status code per line, e.g. 403.'),
268     ];
269
270     // Buttons are only required for testing and debugging reasons.
271     $description = '<p>' . $this->t('These actions will either clear all link checker tables in the database and/or analyze all selected content types, blocks and fields (see settings above) for new/updated/removed links. Normally there is no need to press one of these buttons. Use this only for immediate cleanup tasks and to force a full re-build of the links to be checked in the linkchecker tables. Keep in mind that all custom link settings will be lost if you clear link data!') . '</p>';
272     $description .= '<p>' . $this->t('<strong>Note</strong>: These functions ONLY collect the links, they do not evaluate the HTTP response codes, this will be done during normal cron runs.') . '</p>';
273
274     $form['clear'] = [
275       '#type' => 'details',
276       '#title' => $this->t('Maintenance'),
277       '#description' => $description,
278       '#open' => FALSE,
279     ];
280     $form['clear']['linkchecker_analyze'] = [
281       '#type' => 'submit',
282       '#value' => $this->t('Reanalyze content for links'),
283       '#submit' => ['::submitForm', '::submitAnalyzeLinks'],
284     ];
285     $form['clear']['linkchecker_clear_analyze'] = [
286       '#type' => 'submit',
287       '#value' => $this->t('Clear link data and analyze content for links'),
288       '#submit' => ['::submitForm', '::submitClearAnalyzeLinks'],
289     ];
290
291     return parent::buildForm($form, $form_state);
292   }
293
294   /**
295    * {@inheritdoc}
296    */
297   public function validateForm(array &$form, FormStateInterface $form_state) {
298     parent::validateForm($form, $form_state);
299
300     $form_state->setValue('linkchecker_disable_link_check_for_urls', trim($form_state->getValue('linkchecker_disable_link_check_for_urls')));
301     $form_state->setValue('linkchecker_ignore_response_codes', trim($form_state->getValue('linkchecker_ignore_response_codes')));
302     $ignore_response_codes = preg_split('/(\r\n?|\n)/', $form_state->getValue('linkchecker_ignore_response_codes'));
303     foreach ($ignore_response_codes as $ignore_response_code) {
304       if (!_linkchecker_isvalid_response_code($ignore_response_code)) {
305         $form_state->setErrorByName('linkchecker_ignore_response_codes', $this->t('Invalid response code %code found.', ['%code' => $ignore_response_code]));
306       }
307     }
308
309     // @fixme: remove constant?
310     // Prevent the removal of RFC documentation domains. This are the official and
311     // reserved documentation domains and not "example" hostnames!
312     $linkchecker_disable_link_check_for_urls = array_filter(preg_split('/(\r\n?|\n)/', $form_state->getValue('linkchecker_disable_link_check_for_urls')));
313     $form_state->setValue('linkchecker_disable_link_check_for_urls', implode("\n", array_unique(array_merge(explode("\n", LINKCHECKER_RESERVED_DOCUMENTATION_DOMAINS), $linkchecker_disable_link_check_for_urls))));
314
315     // Validate impersonation user name.
316     $linkchecker_impersonate_account = user_load_by_name($form_state->getValue('linkchecker_impersonate_account'));
317 // @TODO: Cleanup
318 //    if (empty($linkchecker_impersonate_account->id())) {
319     if ($linkchecker_impersonate_account && empty($linkchecker_impersonate_account->id())) {
320       $form_state->setErrorByName('linkchecker_impersonate_account', $this->t('User account %name cannot found.', ['%name' => $form_state->getValue('linkchecker_impersonate_account')]));
321     }
322   }
323
324   /**
325    * {@inheritdoc}
326    */
327   public function submitForm(array &$form, FormStateInterface $form_state) {
328     $config = $this->config('linkchecker.settings');
329     $config
330       ->set('scan_blocks', $form_state->getValue('linkchecker_scan_blocks'))
331       ->set('check_links_types', $form_state->getValue('linkchecker_check_links_types'))
332       ->set('extract.from_a', $form_state->getValue('linkchecker_extract_from_a'))
333       ->set('extract.from_audio', $form_state->getValue('linkchecker_extract_from_audio'))
334       ->set('extract.from_embed', $form_state->getValue('linkchecker_extract_from_embed'))
335       ->set('extract.from_iframe', $form_state->getValue('linkchecker_extract_from_iframe'))
336       ->set('extract.from_img', $form_state->getValue('linkchecker_extract_from_img'))
337       ->set('extract.from_object', $form_state->getValue('linkchecker_extract_from_object'))
338       ->set('extract.from_video', $form_state->getValue('linkchecker_extract_from_video'))
339       ->set('extract.filter_blacklist', $form_state->getValue('linkchecker_filter_blacklist'))
340       ->set('check.connections_max', $form_state->getValue('linkchecker_check_connections_max'))
341       ->set('check.disable_link_check_for_urls', $form_state->getValue('linkchecker_disable_link_check_for_urls'))
342       ->set('check.library', $form_state->getValue('linkchecker_check_library'))
343       ->set('check.interval', $form_state->getValue('linkchecker_check_interval'))
344       ->set('check.useragent', $form_state->getValue('linkchecker_check_useragent'))
345       ->set('error.action_status_code_301', $form_state->getValue('linkchecker_action_status_code_301'))
346       ->set('error.action_status_code_404', $form_state->getValue('linkchecker_action_status_code_404'))
347       ->set('error.ignore_response_codes', $form_state->getValue('linkchecker_ignore_response_codes'))
348       ->set('error.impersonate_account', $form_state->getValue('linkchecker_impersonate_account'))
349       ->set('logging.level', $form_state->getValue('linkchecker_logging_level'))
350       ->save();
351
352     // If block scanning has been selected.
353     if ($form_state->getValue('linkchecker_scan_blocks') > $form['general']['linkchecker_scan_blocks']['#default_value']) {
354       module_load_include('inc', 'linkchecker', 'linkchecker.batch');
355       batch_set(_linkchecker_batch_import_block_custom());
356     }
357
358     parent::submitForm($form, $form_state);
359   }
360
361   /**
362    * Analyze fields in all node types, comments, custom blocks.
363    */
364   function submitAnalyzeLinks(array &$form, FormStateInterface $form_state) {
365     // Start batch and analyze all nodes.
366     $node_types = linkchecker_scan_node_types();
367     if (!empty($node_types)) {
368       module_load_include('inc', 'linkchecker', 'linkchecker.batch');
369       batch_set(_linkchecker_batch_import_nodes($node_types));
370     }
371
372     $comment_types = linkchecker_scan_comment_types();
373     if (!empty($comment_types)) {
374       module_load_include('inc', 'linkchecker', 'linkchecker.batch');
375       batch_set(_linkchecker_batch_import_comments($comment_types));
376     }
377
378     if ($this->config('linkchecker.settings')->get('scan_blocks')) {
379       module_load_include('inc', 'linkchecker', 'linkchecker.batch');
380       batch_set(_linkchecker_batch_import_block_custom());
381     }
382   }
383
384   /**
385    * Clear link data and analyze fields in all content types, comments, custom
386    * blocks.
387    */
388   function submitClearAnalyzeLinks(array &$form, FormStateInterface $form_state) {
389     \Drupal::database()->truncate('linkchecker_block_custom')->execute();
390     \Drupal::database()->truncate('linkchecker_comment')->execute();
391     \Drupal::database()->truncate('linkchecker_node')->execute();
392     \Drupal::database()->truncate('linkchecker_link')->execute();
393
394     // Start batch and analyze all nodes.
395     $node_types = linkchecker_scan_node_types();
396     if (!empty($node_types)) {
397       module_load_include('inc', 'linkchecker', 'linkchecker.batch');
398       batch_set(_linkchecker_batch_import_nodes($node_types));
399     }
400
401     $comment_types = linkchecker_scan_comment_types();
402     if (!empty($comment_types)) {
403       module_load_include('inc', 'linkchecker', 'linkchecker.batch');
404       batch_set(_linkchecker_batch_import_comments($comment_types));
405     }
406
407     if ($this->config('linkchecker.settings')->get('scan_blocks')) {
408       module_load_include('inc', 'linkchecker', 'linkchecker.batch');
409       batch_set(_linkchecker_batch_import_block_custom());
410     }
411   }
412
413 }