From f5c5da742da1154c26277df7ff86eebea6ffe049 Mon Sep 17 00:00:00 2001 From: friendica Date: Fri, 24 Feb 2012 20:03:13 -0800 Subject: attribution issue - don't take author name from contact if available from item. --- include/delivery.php | 6 +++--- include/items.php | 22 ++++++++++++++++------ include/notifier.php | 10 +++++----- 3 files changed, 24 insertions(+), 14 deletions(-) (limited to 'include') diff --git a/include/delivery.php b/include/delivery.php index 677d89388..cbf602a0b 100755 --- a/include/delivery.php +++ b/include/delivery.php @@ -272,10 +272,10 @@ function delivery_run($argv, $argc){ if($normal_mode) { if($item_id == $item['id'] || $item['id'] == $item['parent']) - $atom .= atom_entry($item,'text',$item_contact,$owner,true); + $atom .= atom_entry($item,'text',null,$owner,true); } else - $atom .= atom_entry($item,'text',$item_contact,$owner,true); + $atom .= atom_entry($item,'text',null,$owner,true); } @@ -363,7 +363,7 @@ function delivery_run($argv, $argc){ continue; if(($top_level) && ($public_message) && ($item['author-link'] === $item['owner-link']) && (! $expire)) - $slaps[] = atom_entry($item,'html',$item_contact,$owner,true); + $slaps[] = atom_entry($item,'html',null,$owner,true); } logger('notifier: slapdelivery: ' . $contact['name']); diff --git a/include/items.php b/include/items.php index 347826042..ba4ec036f 100755 --- a/include/items.php +++ b/include/items.php @@ -1505,13 +1505,18 @@ function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) $item_id = $item->get_id(); $datarray = get_atom_elements($feed,$item); - if(! x($datarray,'author-name')) + + if((! x($datarray,'author-name')) && ($contact['network'] != NETWORK_DFRN)) $datarray['author-name'] = $contact['name']; - if(! x($datarray,'author-link')) + if((! x($datarray,'author-link')) && ($contact['network'] != NETWORK_DFRN)) $datarray['author-link'] = $contact['url']; - if(! x($datarray,'author-avatar')) + if((! x($datarray,'author-avatar')) && ($contact['network'] != NETWORK_DFRN)) $datarray['author-avatar'] = $contact['thumb']; + if((! x($datarray,'author-name')) || (! x($datarray,'author-link'))) { + logger('consume_feed: no author information! ' . print_r($datarray,true)); + continue; + } $r = q("SELECT `uid`, `last-child`, `edited`, `body` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($item_id), @@ -1614,14 +1619,19 @@ function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) $datarray = get_atom_elements($feed,$item); if(is_array($contact)) { - if(! x($datarray,'author-name')) + if((! x($datarray,'author-name')) && ($contact['network'] != NETWORK_DFRN)) $datarray['author-name'] = $contact['name']; - if(! x($datarray,'author-link')) + if((! x($datarray,'author-link')) && ($contact['network'] != NETWORK_DFRN)) $datarray['author-link'] = $contact['url']; - if(! x($datarray,'author-avatar')) + if((! x($datarray,'author-avatar')) && ($contact['network'] != NETWORK_DFRN)) $datarray['author-avatar'] = $contact['thumb']; } + if((! x($datarray,'author-name')) || (! x($datarray,'author-link'))) { + logger('consume_feed: no author information! ' . print_r($datarray,true)); + continue; + } + // special handling for events if((x($datarray,'object-type')) && ($datarray['object-type'] === ACTIVITY_OBJ_EVENT)) { diff --git a/include/notifier.php b/include/notifier.php index 37bc6dae5..8ec625286 100755 --- a/include/notifier.php +++ b/include/notifier.php @@ -382,8 +382,8 @@ function notifier_run($argv, $argc){ continue; if($item['id'] == $item_id) { logger('notifier: followup: item: ' . print_r($item,true), LOGGER_DATA); - $slap = atom_entry($item,'html',$owner,$owner,false); - $atom .= atom_entry($item,'text',$owner,$owner,false); + $slap = atom_entry($item,'html',null,$owner,false); + $atom .= atom_entry($item,'text',null,$owner,false); } } } @@ -410,13 +410,13 @@ function notifier_run($argv, $argc){ // older sites without a corresponding dfrn_notify change may do the wrong thing. if($item_id == $item['id'] || $item['id'] == $item['parent']) - $atom .= atom_entry($item,'text',$contact,$owner,true); + $atom .= atom_entry($item,'text',null,$owner,true); } else - $atom .= atom_entry($item,'text',$contact,$owner,true); + $atom .= atom_entry($item,'text',null,$owner,true); if(($top_level) && ($public_message) && ($item['author-link'] === $item['owner-link']) && (! $expire)) - $slaps[] = atom_entry($item,'html',$contact,$owner,true); + $slaps[] = atom_entry($item,'html',null,$owner,true); } } } -- cgit v1.2.3 From a3cf563b6836391ff4d07335cf25189c7a06418e Mon Sep 17 00:00:00 2001 From: friendica Date: Fri, 24 Feb 2012 20:40:09 -0800 Subject: update author name across items if changed. --- include/items.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'include') diff --git a/include/items.php b/include/items.php index ba4ec036f..de3ade6b4 100755 --- a/include/items.php +++ b/include/items.php @@ -1308,12 +1308,28 @@ function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) } if((is_array($contact)) && ($name_updated) && (strlen($new_name)) && ($name_updated > $contact['name-date'])) { - q("UPDATE `contact` SET `name` = '%s', `name-date` = '%s' WHERE `uid` = %d AND `id` = %d LIMIT 1", + $r = q("select * from contact where uid = %d and id = %d limit 1", + intval($contact['uid']), + intval($contact['id']) + ); + + $x = q("UPDATE `contact` SET `name` = '%s', `name-date` = '%s' WHERE `uid` = %d AND `id` = %d LIMIT 1", dbesc(notags(trim($new_name))), dbesc(datetime_convert()), intval($contact['uid']), intval($contact['id']) ); + + // do our best to update the name on content items + + if(count($r)) { + q("update item set `author-name` = '%s' where `author-name` = '%s' and `author-link` = '%s' and uid = %d", + dbesc(notags(trim($new_name))), + dbesc($r[0]['name']), + dbesc($r[0]['url']), + intval($contact['uid']) + ); + } } if(strlen($birthday)) { -- cgit v1.2.3 From 9441ff5142551d1db547ac7ff80c897a52c934c7 Mon Sep 17 00:00:00 2001 From: friendica Date: Fri, 24 Feb 2012 22:47:43 -0800 Subject: string update for 2.38 --- include/diaspora.php | 3 +++ 1 file changed, 3 insertions(+) (limited to 'include') diff --git a/include/diaspora.php b/include/diaspora.php index 92f3500f2..5896f1bfd 100755 --- a/include/diaspora.php +++ b/include/diaspora.php @@ -1803,6 +1803,9 @@ function diaspora_profile($importer,$xml) { if(substr($birthday,5) === substr($contact['bd'],5)) $birthday = $contact['bd']; + // TODO: update name on item['author-name'] if the name changed. See consume_feed() + // Not doing this currently because D* protocol is scheduled for revision soon. + $r = q("UPDATE `contact` SET `name` = '%s', `name-date` = '%s', `photo` = '%s', `thumb` = '%s', `micro` = '%s', `avatar-date` = '%s' , `bd` = '%s' WHERE `id` = %d AND `uid` = %d LIMIT 1", dbesc($name), dbesc(datetime_convert()), -- cgit v1.2.3 From 18bf5d14023d5f6ea4d490362b2825f7a71f951e Mon Sep 17 00:00:00 2001 From: friendica Date: Sat, 25 Feb 2012 00:30:14 -0800 Subject: theme info (like plugin info) --- include/plugin.php | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) (limited to 'include') diff --git a/include/plugin.php b/include/plugin.php index 85b51edff..57f77cb57 100755 --- a/include/plugin.php +++ b/include/plugin.php @@ -166,17 +166,73 @@ function call_hooks($name, &$data = null) { if (! function_exists('get_plugin_info')){ function get_plugin_info($plugin){ - if (!is_file("addon/$plugin/$plugin.php")) return false; + $info=Array( + 'name' => $plugin, + 'description' => "", + 'author' => array(), + 'version' => "" + ); + + if (!is_file("addon/$plugin/$plugin.php")) return $info; $f = file_get_contents("addon/$plugin/$plugin.php"); $r = preg_match("|/\*.*\*/|msU", $f, $m); + if ($r){ + $ll = explode("\n", $m[0]); + foreach( $ll as $l ) { + $l = trim($l,"\t\n\r */"); + if ($l!=""){ + list($k,$v) = array_map("trim", explode(":",$l,2)); + $k= strtolower($k); + if ($k=="author"){ + $r=preg_match("|([^<]+)<([^>]+)>|", $v, $m); + if ($r) { + $info['author'][] = array('name'=>$m[1], 'link'=>$m[2]); + } else { + $info['author'][] = array('name'=>$v); + } + } else { + if (array_key_exists($k,$info)){ + $info[$k]=$v; + } + } + + } + } + + } + return $info; +}} + + +/* + * parse theme comment in search of theme infos. + * like + * + * * Name: My Theme + * * Description: My Cool Theme + * * Version: 1.2.3 + * * Author: John + * * Maintainer: Jane + * * + */ + +if (! function_exists('get_theme_info')){ +function get_theme_info($theme){ $info=Array( - 'name' => $plugin, + 'name' => $theme, 'description' => "", 'author' => array(), + 'maintainer' => array(), 'version' => "" ); + + if (!is_file("view/theme/$theme/theme.php")) return $info; + + $f = file_get_contents("view/theme/$theme/theme.php"); + $r = preg_match("|/\*.*\*/|msU", $f, $m); + if ($r){ $ll = explode("\n", $m[0]); @@ -192,6 +248,14 @@ function get_plugin_info($plugin){ } else { $info['author'][] = array('name'=>$v); } + } + elseif ($k=="maintainer"){ + $r=preg_match("|([^<]+)<([^>]+)>|", $v, $m); + if ($r) { + $info['maintainer'][] = array('name'=>$m[1], 'link'=>$m[2]); + } else { + $info['maintainer'][] = array('name'=>$v); + } } else { if (array_key_exists($k,$info)){ $info[$k]=$v; -- cgit v1.2.3 From 2c7da5d0de29cd7010bd6a34c0b6cfb79fe24466 Mon Sep 17 00:00:00 2001 From: friendica Date: Sat, 25 Feb 2012 14:22:51 -0800 Subject: scale external images --- include/bb2diaspora.php | 32 +------------------------------- include/diaspora.php | 12 ++++++------ include/network.php | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 37 deletions(-) (limited to 'include') diff --git a/include/bb2diaspora.php b/include/bb2diaspora.php index bcef86616..8487f845a 100755 --- a/include/bb2diaspora.php +++ b/include/bb2diaspora.php @@ -55,42 +55,12 @@ function diaspora2bb($s) { $s = preg_replace("/(\[code\])+(.*?)(\[\/code\])+/ism","[code]$2[/code]", $s); // Don't show link to full picture (until it is fixed) - $s = scale_diaspora_images($s, false); + $s = scale_external_images($s, false); return $s; } -function scale_diaspora_images($s,$include_link = true) { - - $matches = null; - $c = preg_match_all('/\[img\](.*?)\[\/img\]/ism',$s,$matches,PREG_SET_ORDER); - if($c) { - require_once('include/Photo.php'); - foreach($matches as $mtch) { - logger('scale_diaspora_image: ' . $mtch[1]); - $i = fetch_url($mtch[1]); - if($i) { - $ph = new Photo($i); - if($ph->is_valid()) { - if($ph->getWidth() > 600 || $ph->getHeight() > 600) { - $ph->scaleImage(600); - $new_width = $ph->getWidth(); - $new_height = $ph->getHeight(); - logger('scale_diaspora_image: ' . $new_width . 'w ' . $new_height . 'h' . 'match: ' . $mtch[0], LOGGER_DEBUG); - $s = str_replace($mtch[0],'[img=' . $new_width . 'x' . $new_height. ']' . $mtch[1] . '[/img]' - . "\n" . (($include_link) - ? '[url=' . $mtch[1] . ']' . t('view full size') . '[/url]' . "\n" - : ''),$s); - logger('scale_diaspora_image: new string: ' . $s, LOGGER_DEBUG); - } - } - } - } - } - return $s; -} - function stripdcode_br_cb($s) { return '[code]' . str_replace('
', "\n\t", $s[1]) . '[/code]'; } diff --git a/include/diaspora.php b/include/diaspora.php index 5896f1bfd..dca857a19 100755 --- a/include/diaspora.php +++ b/include/diaspora.php @@ -794,15 +794,15 @@ function diaspora_reshare($importer,$xml) { if(strlen($source_xml->post->asphoto->objectId) && ($source_xml->post->asphoto->objectId != 0) && ($source_xml->post->asphoto->image_url)) { $body = '[url=' . notags(unxmlify($source_xml->post->asphoto->image_url)) . '][img]' . notags(unxmlify($source_xml->post->asphoto->objectId)) . '[/img][/url]' . "\n"; - $body = scale_diaspora_images($body,false); + $body = scale_external_images($body,false); } elseif($source_xml->post->asphoto->image_url) { $body = '[img]' . notags(unxmlify($source_xml->post->asphoto->image_url)) . '[/img]' . "\n"; - $body = scale_diaspora_images($body); + $body = scale_external_images($body); } elseif($source_xml->post->status_message) { $body = diaspora2bb($source_xml->post->status_message->raw_message); - $body = scale_diaspora_images($body); + $body = scale_external_images($body); } else { @@ -945,11 +945,11 @@ function diaspora_asphoto($importer,$xml) { if(strlen($xml->objectId) && ($xml->objectId != 0) && ($xml->image_url)) { $body = '[url=' . notags(unxmlify($xml->image_url)) . '][img]' . notags(unxmlify($xml->objectId)) . '[/img][/url]' . "\n"; - $body = scale_diaspora_images($body,false); + $body = scale_external_images($body,false); } elseif($xml->image_url) { $body = '[img]' . notags(unxmlify($xml->image_url)) . '[/img]' . "\n"; - $body = scale_diaspora_images($body); + $body = scale_external_images($body); } else { logger('diaspora_asphoto: no photo url found.'); @@ -1476,7 +1476,7 @@ function diaspora_photo($importer,$xml,$msg) { $link_text = '[img]' . $remote_photo_path . $remote_photo_name . '[/img]' . "\n"; - $link_text = scale_diaspora_images($link_text); + $link_text = scale_external_images($link_text); if(strpos($parent_item['body'],$link_text) === false) { $r = q("update item set `body` = '%s', `visible` = 1 where `id` = %d and `uid` = %d limit 1", diff --git a/include/network.php b/include/network.php index 25db62d16..531c3ea4c 100755 --- a/include/network.php +++ b/include/network.php @@ -776,3 +776,43 @@ function add_fcontact($arr,$update = false) { return $r; } + + +function scale_external_images($s,$include_link = true) { + + $a = get_app(); + + $matches = null; + $c = preg_match_all('/\[img\](.*?)\[\/img\]/ism',$s,$matches,PREG_SET_ORDER); + if($c) { + require_once('include/Photo.php'); + foreach($matches as $mtch) { + logger('scale_external_image: ' . $mtch[1]); + $hostname = str_replace('www.','',substr($a->get_baseurl(),strpos($a->get_baseurl(),'://')+3)); + if(stristr($mtch[1],$hostname)) + continue; + $i = fetch_url($mtch[1]); + if($i) { + $ph = new Photo($i); + if($ph->is_valid()) { + $orig_width = $ph->getWidth(); + $orig_height = $ph->getHeight(); + + if($orig_width > 640 || $orig_height > 640) { + + $ph->scaleImage(640); + $new_width = $ph->getWidth(); + $new_height = $ph->getHeight(); + logger('scale_external_images: ' . $orig_width . '->' . $new_width . 'w ' . $orig_height . '->' . $new_height . 'h' . ' match: ' . $mtch[0], LOGGER_DEBUG); + $s = str_replace($mtch[0],'[img=' . $new_width . 'x' . $new_height. ']' . $mtch[1] . '[/img]' + . "\n" . (($include_link) + ? '[url=' . $mtch[1] . ']' . t('view full size') . '[/url]' . "\n" + : ''),$s); + logger('scale_external_images: new string: ' . $s, LOGGER_DEBUG); + } + } + } + } + } + return $s; +} -- cgit v1.2.3 From bcfac6c72f88f9f21d0e7a4f0d0b7568a3dc28d2 Mon Sep 17 00:00:00 2001 From: "Abinoam P. Marques Jr" Date: Sat, 25 Feb 2012 13:23:00 -0800 Subject: include/items.php pass 'parent' to notification --- include/items.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'include') diff --git a/include/items.php b/include/items.php index de3ade6b4..c461b83b4 100755 --- a/include/items.php +++ b/include/items.php @@ -2223,7 +2223,8 @@ function local_delivery($importer,$data) { 'source_photo' => ((link_compare($datarray['author-link'],$importer['url'])) ? $importer['thumb'] : $datarray['author-avatar']), 'verb' => ACTIVITY_POST, - 'otype' => 'item' + 'otype' => 'item', + 'parent' => $parent, )); @@ -2317,7 +2318,7 @@ function local_delivery($importer,$data) { if($datarray['type'] != 'activity') { - $myconv = q("SELECT `author-link`, `author-avatar` FROM `item` WHERE `parent-uri` = '%s' AND `uid` = %d AND `parent` != 0 ", + $myconv = q("SELECT `author-link`, `author-avatar`, `parent` FROM `item` WHERE `parent-uri` = '%s' AND `uid` = %d AND `parent` != 0 ", dbesc($parent_uri), intval($importer['importer_uid']) ); @@ -2330,6 +2331,8 @@ function local_delivery($importer,$data) { continue; require_once('include/enotify.php'); + + $conv_parent = $conv['parent']; notification(array( 'type' => NOTIFY_COMMENT, @@ -2345,7 +2348,8 @@ function local_delivery($importer,$data) { 'source_photo' => ((link_compare($datarray['author-link'],$importer['url'])) ? $importer['thumb'] : $datarray['author-avatar']), 'verb' => ACTIVITY_POST, - 'otype' => 'item' + 'otype' => 'item', + 'parent' => $conv_parent, )); -- cgit v1.2.3 From f3a225437b4690e0d02c773600d29837674660d7 Mon Sep 17 00:00:00 2001 From: "Abinoam P. Marques Jr" Date: Sat, 25 Feb 2012 16:56:14 -0800 Subject: Comment notifications by e-mail now are threaded --- include/enotify.php | 56 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 6 deletions(-) (limited to 'include') diff --git a/include/enotify.php b/include/enotify.php index 1eb3b5476..33e083b5e 100755 --- a/include/enotify.php +++ b/include/enotify.php @@ -13,7 +13,9 @@ function notification($params) { $site_admin = sprintf( t('%s Administrator'), $sitename); $sender_name = $product; - $sender_email = t('noreply') . '@' . $a->get_hostname(); + $hostname = $a->get_hostname(); + $sender_email = t('noreply') . '@' . $hostname; + $additional_mail_header = ""; if(array_key_exists('item',$params)) { $title = $params['item']['title']; @@ -36,8 +38,15 @@ function notification($params) { } if($params['type'] == NOTIFY_COMMENT) { + logger("notification: params = " . print_r($params, true), LOGGER_DEBUG); - $subject = sprintf( t('%s commented on an item at %s'), $params['source_name'], $sitename); + $parent_id = $params['parent']; + + // Some mail softwares relies on subject field for threading. + // So, we cannot have different subjects for notifications of the same thread. + // Before this we have the name of the replier on the subject rendering + // differents subjects for messages on the same thread. + $subject = sprintf( t('Someone commented on item #%d at %s'), $parent_id, $sitename); $preamble = sprintf( t('%s commented on an item/conversation you have been following.'), $params['source_name']); $epreamble = sprintf( t('%s commented in %s.'), '[url=' . $params['source_link'] . ']' . $params['source_name'] . '[/url]', '[url=$itemlink]' . t('a watched conversation') . '[/url]'); @@ -126,8 +135,6 @@ function notification($params) { } while($dups == true); - - // create notification entry in DB $r = q("insert into notify (hash,name,url,photo,date,uid,link,type,verb,otype) @@ -170,6 +177,40 @@ function notification($params) { logger('notification: sending notification email'); + $id_for_parent = "${params['parent']}@${hostname}"; + + // Is this the first email notification for this parent item and user? + + $r = q("select `id` from `notify-threads` where `master-parent-item` = %d and `receiver-uid` = %d limit 1", + intval($params['parent']), + intval($params['uid']) ); + + // If so, create the record of it and use a message-id smtp header. + + if(!$r) { + logger("norify_id:" . intval($notify_id). ", parent: " . intval($params['parent']) . "uid: " . +intval($params['uid']), LOGGER_DEBUG); + $r = q("insert into `notify-threads` (`notify-id`, `master-parent-item`, `receiver-uid`, `parent-item`) + values(%d,%d,%d,%d)", + intval($notify_id), + intval($params['parent']), + intval($params['uid']), + 0 ); + + $additional_mail_header .= "Message-ID: <${id_for_parent}>\n"; + $log_msg = "include/enotify: No previous notification found for this parent:\n" . + " parent: ${params['parent']}\n" . " uid : ${params['uid']}\n"; + logger($log_msg, LOGGER_DEBUG); + } + + // If not, just "follow" the thread. + + else { + $additional_mail_header = "References: <${id_for_parent}>\nIn-Reply-To: <${id_for_parent}>\n"; + logger("include/enotify: There's already a notification for this parent:\n" . print_r($r, true), LOGGER_DEBUG); + } + + $textversion = strip_tags(html_entity_decode(bbcode(stripslashes(str_replace(array("\\r\\n", "\\r", "\\n"), "\n", $body))),ENT_QUOTES,'UTF-8')); @@ -227,7 +268,8 @@ function notification($params) { 'toEmail' => $params['to_email'], 'messageSubject' => $subject, 'htmlVersion' => $email_html_body, - 'textVersion' => $email_text_body + 'textVersion' => $email_text_body, + 'additionalMailHeader' => $additional_mail_header, )); } @@ -248,6 +290,7 @@ class enotify { * @param messageSubject subject of the message * @param htmlVersion html version of the message * @param textVersion text only version of the message + * @param additionalMailHeader additions to the smtp mail header */ static public function send($params) { @@ -262,6 +305,7 @@ class enotify { // generate a multipart/alternative message header $messageHeader = + $params['additionalMailHeader'] . "From: {$params['fromName']} <{$params['fromEmail']}>\n" . "Reply-To: {$params['fromName']} <{$params['replyTo']}>\n" . "MIME-Version: 1.0\n" . @@ -291,4 +335,4 @@ class enotify { logger("notification: enotify::send returns " . $res, LOGGER_DEBUG); } } -?> \ No newline at end of file +?> -- cgit v1.2.3 From 5f7858a688a1b6877246933a6c4c72318c885109 Mon Sep 17 00:00:00 2001 From: friendica Date: Sun, 26 Feb 2012 04:17:02 -0800 Subject: show experimental and unsupported theme status on theme admin page --- include/plugin.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'include') diff --git a/include/plugin.php b/include/plugin.php index 57f77cb57..8280b1022 100755 --- a/include/plugin.php +++ b/include/plugin.php @@ -225,9 +225,16 @@ function get_theme_info($theme){ 'description' => "", 'author' => array(), 'maintainer' => array(), - 'version' => "" + 'version' => "", + 'experimental' => false, + 'unsupported' => false ); + if(file_exists("view/theme/$theme/experimental")) + $info['experimental'] = true; + if(file_exists("view/theme/$theme/unsupported")) + $info['unsupported'] = true; + if (!is_file("view/theme/$theme/theme.php")) return $info; $f = file_get_contents("view/theme/$theme/theme.php"); -- cgit v1.2.3