/[drupal]/contributions/modules/drupalorg/drupalorg/drupalorg.module
ViewVC logotype

Contents of /contributions/modules/drupalorg/drupalorg/drupalorg.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.38 - (show annotations) (download) (as text)
Sun May 17 17:57:17 2009 UTC (6 months, 1 week ago) by dww
Branch: MAIN
CVS Tags: HEAD
Changes since 1.37: +2 -2 lines
File MIME type: text/x-php
#379072 by webchick, dww, et al: Allow doc team members to attach
files to book pages again (fixed regression from the D6 upgrade).
1 <?php
2 // $Id: drupalorg.module,v 1.37 2009/04/11 21:43:08 dmitrig01 Exp $
3
4 /**
5 * @file
6 * This module contains customizations used on drupal.org itself. It is not
7 * meant to be useful for other sites, except as an example of the kinds of
8 * modifications you can make with a site-specific module.
9 */
10
11 /**
12 * Profile field ID for country.
13 */
14 define('DRUPALORG_COUNTRY_PID', 17);
15
16 /**
17 * Profile field ID for languages.
18 */
19 define('DRUPALORG_LANGUAGES_PID', 48);
20
21 // == Core hooks ===============================================================
22
23 /**
24 * Implementation of hook_menu().
25 */
26 function drupalorg_menu() {
27 $items['documentation'] = array(
28 'title' => 'Documentation',
29 'page callback' => 'drupalorg_documentation',
30 'access arguments' => array('access content'),
31 'type' => MENU_CALLBACK,
32 );
33
34 $items['documentation/index'] = array(
35 'title' => 'Documentation',
36 'page callback' => 'drupalorg_documentation_index',
37 'access arguments' => array('access content'),
38 'type' => MENU_CALLBACK,
39 );
40
41 $items['start'] = array(
42 'title' => 'Getting started with Drupal',
43 'page callback' => 'drupalorg_start',
44 'access arguments' => array('access content'),
45 'type' => MENU_CALLBACK,
46 );
47
48 $items['download'] = array(
49 'title' => 'Download & Extend',
50 'page callback' => 'drupalorg_download',
51 'access arguments' => array('access content'),
52 'type' => MENU_CALLBACK,
53 );
54
55 $items['home'] = array(
56 'title' => 'Drupal Homepage',
57 'page callback' => 'drupalorg_home',
58 'access arguments' => array('access content'),
59 'type' => MENU_CALLBACK,
60 );
61
62 $items['about'] = array(
63 'title' => 'About Drupal',
64 'page callback' => 'drupalorg_about',
65 'access arguments' => array('access content'),
66 'type' => MENU_CALLBACK,
67 );
68
69 $items['community'] = array(
70 'title' => 'Community & Support',
71 'page callback' => 'drupalorg_community',
72 'access arguments' => array('access content'),
73 'type' => MENU_CALLBACK,
74 );
75
76 // Redirect some legacy paths related to the security listings.
77 $items['security-contrib'] = array(
78 'access arguments' => array('access content'),
79 'page callback' => 'drupal_goto',
80 'page arguments' => array('security/contrib'),
81 'type' => MENU_CALLBACK,
82 );
83 $items['security-contrib/rss.xml'] = array(
84 'access arguments' => array('access content'),
85 'page callback' => 'drupal_goto',
86 'page arguments' => array('security/contrib/rss.xml'),
87 'type' => MENU_CALLBACK,
88 );
89 $items['security-psa'] = array(
90 'access arguments' => array('access content'),
91 'page callback' => 'drupal_goto',
92 'page arguments' => array('security/psa'),
93 'type' => MENU_CALLBACK,
94 );
95 $items['security-psa/rss.xml'] = array(
96 'access arguments' => array('access content'),
97 'page callback' => 'drupal_goto',
98 'page arguments' => array('security/psa/rss.xml'),
99 'type' => MENU_CALLBACK,
100 );
101
102 return $items;
103 }
104
105 // == Major landing pages ======================================================
106
107 /**
108 * Page callback for the frontpage.
109 *
110 * @todo
111 * Add caching.
112 */
113 function drupalorg_home() {
114 if (module_exists('jquery_ui')) {
115 jquery_ui_add('ui.tabs');
116 drupal_add_js("Drupal.behaviors.frontTabs = function() { $('#rotate > ul').tabs({ fx: { opacity: 'toggle' } }).tabs('rotate', 0); };", 'inline');
117 }
118 drupal_add_js(drupal_get_path('module', 'drupalorg') .'/js/jquery.bt.min.js');
119 drupal_add_js(drupal_get_path('module', 'drupalorg') .'/js/home.js');
120 drupal_add_js(array('homePageMap' => array(
121 array('content' => '<strong>Test</strong><div class="description">description</div>', 'top' => 122, 'left' => 335, 'type' => 'forum'),
122 array('content' => '<strong>Test2</strong><div class="description">d3scr1p110n</div>', 'top' => 105, 'left' => 52, 'type' => 'forum'),
123 )), 'setting');
124
125 return theme('drupalorg_home');
126 }
127
128 /**
129 * Implementation of template_preprocess_page().
130 */
131 function drupalorg_preprocess_page(&$variables) {
132 if ($variables['is_front']) {
133 $variables['mission'] = '<h2>Build Today. Innovate Tomorrow.</h2>
134 <p class="standfirst">
135 What can you make with Drupal? Beautiful, personal blogs or mighty, multi-featured, multiuser corporate sites. Go wherever your imagination takes you with our robust, open-source publishing software.
136 </p>';
137 }
138
139 // Add extra choices to the dblog settings selector
140 if ($form_id == 'dblog_admin_settings') {
141 $form['dblog_row_limit']['#options'] = drupal_map_assoc(array(100, 1000, 10000, 100000, 250000, 500000, 1000000));
142 }
143 }
144
145 /**
146 * Implementation of template_preprocess_drupalorg_home().
147 */
148 function drupalorg_preprocess_drupalorg_home(&$variables) {
149 $variables['theme_images_directory'] = drupal_get_path('theme', variable_get('theme_default', 'bluecheese')) .'/images';
150
151 // == Forums tab data/markup
152
153 // Performs badly. Need to find a way to make this run quicker. Of course
154 // we need to cache it, but it still runs dog slow.
155 $variables['tab_content_forums'] = 'Recent forums tab content...';
156
157 /*$sql = db_rewrite_sql("SELECT n.nid, n.title, n.created, n.uid, u.name FROM {node} n INNER JOIN {term_node} tn ON tn.vid = n.vid INNER JOIN {term_data} td ON td.tid = tn.tid INNER JOIN {users} u ON u.uid = u.uid WHERE n.status = 1 AND td.vid = %d ORDER BY n.nid DESC");
158 $result = db_query_range($sql, variable_get('forum_nav_vocabulary', ''), 0, 5);
159 $recent_forums = '';
160 while ($node = db_fetch_object($result)) {
161 $recent_forums .= '<h6>'. l($node->title, 'node/'. $node->nid) .'</h6><p class="submitted">'. theme('node_submitted', $node) .'</p>';
162 }
163 // We have no place to link this to in a nice way.
164 // $fresh_news .= '<p>'. l(t('More recent forum topics...'), '...') .'</p>';
165 $variables['tab_content_forums'] = $recent_forums;*/
166
167 // == Counters for users
168
169 // If we do not have data or our data was generated an hour ago,
170 // try to generate it again.
171 if (!count($stats = variable_get('drupalorg_front_counter_data', array())) || (variable_get('drupalorg_front_counter_time', 0) < (time() - 60*60))) {
172 $stats = array();
173 // Users: Count them.
174 $stats['number_of_users'] = number_format(db_result(db_query('SELECT COUNT(*) FROM {users}')));
175
176 // Country: A single text field, so we can filter it in SQL.
177 $stats['number_of_countries'] = db_result(db_query('SELECT COUNT(DISTINCT value) FROM {profile_values} WHERE fid = %d', DRUPALORG_COUNTRY_PID));
178
179 // Languages: Get all the distinct combinations and then filter that down.
180 $languages = array();
181 $result_languages = db_query('SELECT DISTINCT value FROM {profile_values} WHERE fid = %d', DRUPALORG_LANGUAGES_PID);
182 while ($language_profile = db_fetch_object($result_languages)) {
183 $languages = array_merge($languages, explode('; ', $language_profile->value));
184 }
185 $languages = array_unique($languages);
186 $stats['number_of_languages'] = count($languages);
187
188 // Store all the results and the time.
189 variable_set('drupalorg_front_counter_data', $stats);
190 variable_set('drupalorg_front_counter_time', time());
191 }
192 $variables += $stats;
193 }
194
195 /**
196 * Page callback for the documentation page.
197 *
198 * @todo
199 * Add caching.
200 */
201 function drupalorg_documentation() {
202 return theme('drupalorg_documentation');
203 }
204
205 /**
206 * Page callback for the documentation index page.
207 *
208 * @todo
209 * Add caching.
210 */
211 function drupalorg_documentation_index() {
212 return theme('drupalorg_documentation_index');
213 }
214
215 /**
216 * Page callback for the getting started page.
217 *
218 * @todo
219 * Add caching.
220 */
221 function drupalorg_start() {
222 return theme('drupalorg_start');
223 }
224
225 /**
226 * Page callback for the about Druapl page.
227 */
228 function drupalorg_about() {
229 return theme('drupalorg_about');
230 }
231
232 /**
233 * Implementation of template_preprocess_drupalorg_start().
234 *
235 * @todo
236 * Add caching.
237 */
238 function drupalorg_preprocess_drupalorg_start(&$variables) {
239 $variables['theme_images_directory'] = drupal_get_path('theme', variable_get('theme_default', 'bluecheese')) .'/images';
240
241 $variables['version'] = 42;
242
243 $variables['most_popular_modules'] = theme('item_list', array(
244 '<a href="#">Content construction kit (CCK)</a>',
245 '<a href="#">Views</a>',
246 '<a href="#">Gallery</a>',
247 '<a href="#">Foo bar</a>'),
248 NULL, 'ul', array('class' => 'flat')); // Will be made dynamic, see issue #372243
249
250 $variables['most_popular_themes'] = theme('item_list', array(
251 '<a href="#">Paris Eiffel Tower</a>',
252 '<a href="#">Cute yellow ducks</a>',
253 '<a href="#">Bluecheese</a>',
254 '<a href="#">Marvin</a>'),
255 NULL, 'ul', array('class' => 'flat')); // Will be made dynamic, see issue #372243
256 }
257
258 /**
259 * Page callback for the download page.
260 *
261 * @todo
262 * Add caching.
263 */
264 function drupalorg_download() {
265 return theme('drupalorg_download');
266 }
267
268 /**
269 * Implementation of template_preprocess_drupalorg_download().
270 *
271 * @todo
272 * Add caching.
273 */
274 function drupalorg_preprocess_drupalorg_download(&$variables) {
275 $variables['version'] = 42;
276
277 //most popular modules, modules tid = 14
278 //TODO: reference project_usage function instead of drupalorg function
279 $most_popular_modules = drupalorg_get_most_active_projects(14, $limit = 4);
280 $variables['most_popular_modules'] = drupalorg_render_project_listing($most_popular_modules);
281
282 //most popular themes, themes tid = 15
283 //TODO: reference project_usage function instead of drupalorg function
284 $projects_popular_themes = drupalorg_get_most_active_projects(15, $limit = 4);
285 $variables['most_popular_themes'] = drupalorg_render_project_listing($projects_popular_themes);
286
287 $variables['most_active_modules'] = theme('item_list', array(
288 '<a href="#">Content construction kit (CCK)</a>',
289 '<a href="#">Views</a>',
290 '<a href="#">Gallery</a>',
291 '<a href="#">Foo bar</a>'),
292 NULL, 'ul', array('class' => 'flat'));
293
294 $variables['most_active_themes'] = theme('item_list', array(
295 '<a href="#">Paris Eiffel Tower</a>',
296 '<a href="#">Bluecheese</a>',
297 '<a href="#">Cute yellow ducks</a>',
298 '<a href="#">Marvin</a>'),
299 NULL, 'ul', array('class' => 'flat'));
300
301 //new modules, modules tid = 14
302 //TODO: reference project_usage function instead of drupalorg function
303 $new_modules = drupalorg_get_most_recent_projects(14, $limit = 4);
304 $variables['new_modules'] = drupalorg_render_project_listing($new_modules);
305
306 //new themes, themes tid = 15
307 //TODO: reference project_usage function instead of drupalorg function
308 $new_themes = drupalorg_get_most_recent_projects(15, $limit = 4);
309 $variables['new_themes'] = drupalorg_render_project_listing($new_themes);
310
311 $variables['module_categories'] = theme('item_list', array(
312 '<a href="#">Gallery</a>',
313 '<a href="#">Content construction kit (CCK)</a>',
314 '<a href="#">Views</a>',
315 '<a href="#">Foo bar</a>'),
316 NULL, 'ul', array('class' => 'flat'));
317
318 $variables['theme_categories'] = theme('item_list', array(
319 '<a href="#">Cute yellow ducks</a>',
320 '<a href="#">Paris Eiffel Tower</a>',
321 '<a href="#">Bluecheese</a>',
322 '<a href="#">Marvin</a>'),
323 NULL, 'ul', array('class' => 'flat'));
324 }
325
326 /**
327 * Page callback for the community page.
328 *
329 * @todo
330 * Add caching.
331 */
332 function drupalorg_community() {
333 return theme('drupalorg_community');
334 }
335
336 /**
337 * Implementation of template_preprocess_drupalorg_community().
338 *
339 * @todo
340 * Add caching.
341 */
342 function drupalorg_preprocess_drupalorg_community(&$variables) {
343 $variables['advertisement'] = ''; // used to pull in the proper advertisement block.
344 }
345
346 /**
347 * Implementation of hook_theme().
348 */
349 function drupalorg_theme() {
350 return array(
351 'drupalorg_documentation' => array(
352 'template' => 'drupalorg-documentation',
353 ),
354 'drupalorg_documentation_index' => array(
355 'template' => 'drupalorg-documentation-index',
356 ),
357 'drupalorg_start' => array(
358 'template' => 'drupalorg-start',
359 ),
360 'drupalorg_download' => array(
361 'template' => 'drupalorg-download',
362 ),
363 'drupalorg_home' => array(
364 'template' => 'drupalorg-home',
365 ),
366 'drupalorg_about' => array(
367 'template' => 'drupalorg-about',
368 ),
369 'drupalorg_community' => array(
370 'template' => 'drupalorg-community',
371 ),
372 );
373 }
374
375 // == Altering existing behavior ===============================================
376
377 /**
378 * Implementation of hook_form_alter().
379 */
380 function drupalorg_form_alter(&$form, $form_state, $form_id) {
381 // List of forms to check for overrides, and the corresponding permissions.
382 $override_forms = array(
383 'book_node_form' => 'revert revisions',
384 'forum_node_form' => 'administer nodes',
385 'page_node_form' => 'administer nodes',
386 'story_node_form' => 'administer nodes',
387 );
388 // Override the access for attachments if it's a forbidden form,
389 // and the user does not have sufficient permissions.
390 if (in_array($form_id, array_keys($override_forms)) && !user_access($override_forms[$form_id])) {
391 if (isset($form['attachments']['#access'])) {
392 $form['attachments']['#access'] = FALSE;
393 }
394 }
395
396 // Ensure that wildcard email addresses are not abused.
397 if ($form_id == 'user_register') {
398 $form['#validate'][] = 'drupalorg_register_mail_validate';
399 }
400
401 // Core search index is not used, so clear off wipe option.
402 if ($form_id == 'search_admin_settings') {
403 unset($form['status']['wipe']);
404 }
405
406 // Add home page option to user access rule adding and editing.
407 // Make sure if we edit a homepage option, we keep using that as a default.
408 if ($form_id == 'user_admin_access_add_form' || $form_id == 'user_admin_access_edit_form') {
409 if ($form['#parameters'][2]['type'] == 'homepage') {
410 $form['type']['#default_value'] = 'homepage';
411 }
412 $form['type']['#options']['homepage'] = t('Homepage');
413 }
414
415 // Ensure nice Drupal home page addresses
416 if ($form_id == 'user_profile_form') {
417 $form['#validate'][] = 'drupalorg_profile_user_edit_validate';
418 // Hack to make the language list a multiselect field (there is no UI
419 // for this in profile module). We need to hack around that profile only
420 // ever stores select field values as strings, so we need to explode what
421 // was in there for our multiselect form.
422 if (isset($form['Personal information']['profile_languages'])) {
423 $form['Personal information']['profile_languages']['#multiple'] = TRUE;
424 $form['Personal information']['profile_languages']['#default_value'] = explode('; ', $form['Personal information']['profile_languages']['#default_value']);
425 $form['#submit'] = array_merge(array('drupalorg_profile_fix_languages'), $form['#submit']);
426 }
427 }
428 }
429
430 // == User form functionality ==================================================
431
432 /**
433 * Try to catch wildcard email address signups, such as joe+drupal@gmail.com.
434 */
435 function drupalorg_register_mail_validate($form, &$form_state) {
436 $hit = preg_match('/(.*)\+(.*)\@(.*)/', $form_state['values']['mail'], $match);
437 if ($hit) {
438 if (db_result(db_query("SELECT uid FROM {users} WHERE LOWER(mail) LIKE LOWER('%s')", $match[1] .'+%%@'. $match[3])) > 0) {
439 form_set_error('mail', t('An e-mail address similar to %email is already registered. <a href="@password">Have you forgotten your password?</a>', array('%email' => $form_state['values']['mail'], '@password' => url('user/password'))));
440 }
441 }
442 }
443
444 /**
445 * Validate all fields in the user_edit form against the list of bad words.
446 *
447 * @todo Core almost supports it with above form_alter but listings are bad
448 * (when the value is homepage, core does not know about it, so does not print it).
449 * @todo Headers are blocked by some providers so this is not accurate.
450 */
451 function drupalorg_profile_user_edit_validate($form, &$form_state) {
452 if (!empty($form_state['values']['homepage']) && is_string($form_state['values']['homepage']) && (strlen($form_state['values']['homepage']) > 7)) {
453 $result = db_query("SELECT mask FROM {access} WHERE type = '%s' AND status = %d", 'homepage', 0);
454 $masks = array();
455 while ($mask = db_fetch_object($result)) {
456 // Build masks array for preg_matching.
457 $masks[] = '@'. strtr($mask->mask, array('.' => '\.', '%' => '.*', '_' => '.')) .'@';
458 }
459 // Check denied homepages.
460 foreach ($masks as $mask) {
461 if (preg_match($mask, $form_state['values']['homepage'])) {
462 form_set_error('homepage', t('Unsuitable Drupal site detected. This address cannot be set as your Drupal site link.'));
463 }
464 }
465 // Did not work due to several hosts rewriting headers.
466 /*if (!user_access('administer users')) {
467 // Check for Drupal-ness of website. Try only once.
468 $response = drupal_http_request($form_state['values']['homepage'], array(), 'GET', NULL, 1);
469 if ($response->headers['Expires'] != 'Sun, 19 Nov 1978 05:00:00 GMT') {
470 form_set_error('homepage', t("Your website does not seem to be a Drupal site. If you think we are wrong, please open an issue in the webmasters' queue."));
471 }
472 }*/
473 }
474 }
475
476 /**
477 * Submit handler for the user profile form, to serialize languages to a string.
478 */
479 function drupalorg_profile_fix_languages(&$form, &$form_state) {
480 if (is_array($form_state['values']['profile_languages'])) {
481 $form_state['values']['profile_languages'] = join('; ', array_keys($form_state['values']['profile_languages']));
482 }
483 }
484
485 // == External search block ====================================================
486
487 /**
488 * Implementation of hook_block().
489 *
490 * @todo Hopefully remove as part of search migration.
491 */
492 function drupalorg_block($op = 'list', $delta = 0, $edit = array()) {
493 if ($op == 'list') {
494 $blocks[0] = array('info' => t('External/Alternate Search Advice (only when search is disabled)'),
495 'weight' => 0, 'enabled' => 0, 'region' => 'header');
496 return $blocks;
497 }
498 else if ($op == 'view') {
499 switch ($delta) {
500 case 0:
501 $block = array('subject' => t('Search Engine'),
502 'content' => drupalorg_display_block_external_search());
503 break;
504 }
505 return $block;
506 }
507 }
508
509 /**
510 * Body for external search block.
511 */
512 function drupalorg_display_block_external_search() {
513 if (!user_access('search content')) {
514 $form = '<form method="get" action="http://www.google.com/search"><div>
515 <input type="hidden" name="ie" value="UTF-8" />
516 <input type="hidden" name="oe" value="UTF-8" />
517 <input type="hidden" name="domains" value="drupal.org" />
518 <input type="hidden" name="sitesearch" value="drupal.org" />
519 <input type="text" class="form-text" name="q" size="20" maxlength="255" value="" />
520 <input type="submit" class="form-submit" name="btnG" value="Google Search" /></div>
521 </form>';
522 $message = '<p>Due to load issues the Drupal.org search occasionally has to be disabled. When this happens, you can use external search engines and a modifier like "site:drupal.org" to refine your results to Drupal. For more information <a href="http://drupal.org/node/271694">see the infrastructure queue</a></p>.';
523 return $form . $message;
524 }
525 }
526
527 // == IRC nick search ==========================================================
528
529 /**
530 * Implementation of hook_search().
531 *
532 * Add support for searching for users based on the fixed IRC nickname field.
533 *
534 * @todo Possibly remove as part of search migration.
535 */
536 function drupalorg_search($op = 'search', $keys = NULL) {
537 switch ($op) {
538 case 'name':
539 if (user_access('access user profiles')) {
540 return t('IRC nicks');
541 }
542 case 'search':
543 if (user_access('access user profiles')) {
544 $find = array();
545 // Replace wildcards with MySQL/PostgreSQL wildcards.
546 $keys = preg_replace('!\*+!', '%', $keys);
547 $result = pager_query("SELECT u.name, u.uid FROM {profile_values} pv INNER JOIN {users} u ON pv.uid = u.uid WHERE pv.fid = 35 AND LOWER(pv.value) LIKE LOWER('%%%s%%')", 15, 0, NULL, $keys);
548 while ($account = db_fetch_object($result)) {
549 $find[] = array('title' => $account->name, 'link' => url('user/'. $account->uid, array('html' => TRUE)));
550 }
551 return $find;
552 }
553 }
554 }
555
556 /**
557 * Implementation of hook_views_api().
558 */
559 function drupalorg_views_api() {
560 return array(
561 'api' => 2.0,
562 'path' => drupal_get_path('module', 'drupalorg'),
563 );
564 }
565
566 // == Project related output for download page see issue #372243================
567
568 /**
569 * Return an array of the most active projects with the given taxonomy term.
570 *
571 * @todo
572 * move this function into project_usage module - see issue #372243
573 * @return
574 * An array of of the most active projects. The array includes the node
575 * ID and node title.
576 */
577 function drupalorg_get_most_active_projects($tid, $limit = 5) {
578 $result = db_query('SELECT n.nid, n.title FROM {node} n INNER JOIN {project_projects} pp ON n.nid = pp.nid INNER JOIN {project_usage_week_project} pw ON pp.nid = pw.nid INNER JOIN {term_node} tn ON tn.nid = pp.nid WHERE pw.timestamp = (SELECT MAX(timestamp) FROM {project_usage_week_project}) AND tn.tid = %d AND n.status = 1 ORDER BY pw.count DESC LIMIT %d', $tid, $limit);
579 $projects = array();
580 while ($project = db_fetch_object($result)) {
581 $projects[] = $project;
582 }
583 return $projects;
584 }
585
586 /**
587 * Return an array of the most recently created projects with the given taxonomy term.
588 *
589 * @todo
590 * move this function into project_usage module - see issue #372243
591 * @return
592 * An array of of the most recently created projects. The array includes the node
593 * ID and node title.
594 */
595 function drupalorg_get_most_recent_projects($tid, $limit = 5) {
596 $result = db_query('SELECT n.nid, n.title, n.created FROM {node} n INNER JOIN {project_projects} pp ON n.nid = pp.nid INNER JOIN {term_node} tn ON tn.nid = pp.nid WHERE tn.tid = %d AND n.status = 1 ORDER BY n.created DESC LIMIT %d', $tid, $limit);
597 $projects = array();
598 while ($project = db_fetch_object($result)) {
599 $projects[] = $project;
600 }
601 return $projects;
602 }
603
604 /**
605 * Render project listings for download and homepage.
606 *
607 * @return a themed ul listing of projects
608 */
609 function drupalorg_render_project_listing($projects) {
610 foreach ($projects as $project) {
611 $links[] = l($project->title, 'node/'.$project->nid, array());
612 }
613 return theme('item_list', $links, NULL, 'ul', array('class' => 'flat'));
614 }
615
616

  ViewVC Help
Powered by ViewVC 1.1.2