From 47e9613acde8a6282e3c3717edf6efb7fce47252 Mon Sep 17 00:00:00 2001 From: Mike Macgirvin Date: Sat, 14 Aug 2010 07:55:18 -0700 Subject: [PATCH] mongo notification checkin, wall, comments, settings --- include/items.php | 7 +-- include/notifier.php | 6 ++- mod/dfrn_notify.php | 99 +++++++++++++++++++++++++++----------- mod/item.php | 93 +++++++++++++++++++++++++++++------ mod/profile.php | 11 ++++- mod/settings.php | 25 ++++++++-- view/cmnt_received_eml.tpl | 16 ++++++ view/settings.tpl | 20 +++++++- view/style.css | 40 +++++++++++++-- view/wall_received_eml.tpl | 16 ++++++ 10 files changed, 275 insertions(+), 58 deletions(-) create mode 100644 view/cmnt_received_eml.tpl create mode 100644 view/wall_received_eml.tpl diff --git a/include/items.php b/include/items.php index 6877116c98..74809583c3 100644 --- a/include/items.php +++ b/include/items.php @@ -96,8 +96,7 @@ function get_feed_for(&$a,$dfrn_id,$owner_id,$last_update) { '$thumb' => xmlify($owner['thumb']), '$picdate' => xmlify(datetime_convert('UTC','UTC',$owner['avatar-date'] . '+00:00' , 'Y-m-d\TH:i:s\Z')) , '$uridate' => xmlify(datetime_convert('UTC','UTC',$owner['uri-date'] . '+00:00' , 'Y-m-d\TH:i:s\Z')) , - '$namdate' => xmlify(datetime_convert('UTC','UTC',$owner['name-date'] . '+00:00' , 'Y-m-d\TH:i:s\Z')) , - + '$namdate' => xmlify(datetime_convert('UTC','UTC',$owner['name-date'] . '+00:00' , 'Y-m-d\TH:i:s\Z')) )); @@ -109,7 +108,6 @@ function get_feed_for(&$a,$dfrn_id,$owner_id,$last_update) { )); } else { - if($item['parent'] == $item['id']) { $atom .= replace_macros($item_template, array( '$name' => xmlify($item['name']), @@ -143,8 +141,7 @@ function get_feed_for(&$a,$dfrn_id,$owner_id,$last_update) { } } - $atom .= "\r\n"; - + $atom .= '' . "\r\n"; return $atom; } diff --git a/include/notifier.php b/include/notifier.php index 7d9b09da34..36ff27054f 100644 --- a/include/notifier.php +++ b/include/notifier.php @@ -137,7 +137,11 @@ dbg(3); $updated . '+00:00' , 'Y-m-d\TH:i:s\Z')) , '$name' => xmlify($owner['name']), '$profile_page' => xmlify($owner['url']), - '$photo' => xmlify($owner['photo']) + '$photo' => xmlify($owner['photo']), + '$thumb' => xmlify($owner['thumb']), + '$picdate' => xmlify(datetime_convert('UTC','UTC',$owner['avatar-date'] . '+00:00' , 'Y-m-d\TH:i:s\Z')) , + '$uridate' => xmlify(datetime_convert('UTC','UTC',$owner['uri-date'] . '+00:00' , 'Y-m-d\TH:i:s\Z')) , + '$namdate' => xmlify(datetime_convert('UTC','UTC',$owner['name-date'] . '+00:00' , 'Y-m-d\TH:i:s\Z')) )); if($cmd == 'mail') { diff --git a/mod/dfrn_notify.php b/mod/dfrn_notify.php index 03d96d27ea..5f0f3451fa 100644 --- a/mod/dfrn_notify.php +++ b/mod/dfrn_notify.php @@ -23,7 +23,7 @@ function dfrn_notify_post(&$a) { // find the local user who owns this relationship. - $r = q("SELECT * FROM `contact` WHERE `issued-id` = '%s' LIMIT 1", + $r = q("SELECT `contact`.*, `contact`.`uid` AS `importer_uid`, `user`.* FROM `contact` LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid` WHERE `issued-id` = '%s' LIMIT 1", dbesc($dfrn_id) ); if(! count($r)) { @@ -54,7 +54,7 @@ function dfrn_notify_post(&$a) { $base = $rawmail[0]['child'][NAMESPACE_DFRN]; $msg = array(); - $msg['uid'] = $importer['uid']; + $msg['uid'] = $importer['importer_uid']; $msg['from-name'] = notags(unxmlify($base['sender'][0]['child'][NAMESPACE_DFRN]['name'][0]['data'])); $msg['from-photo'] = notags(unxmlify($base['sender'][0]['child'][NAMESPACE_DFRN]['avatar'][0]['data'])); $msg['from-url'] = notags(unxmlify($base['sender'][0]['child'][NAMESPACE_DFRN]['uri'][0]['data'])); @@ -74,24 +74,21 @@ function dfrn_notify_post(&$a) { . "`) VALUES ('" . implode("', '", array_values($msg)) . "')" ); // send email notification if requested. - $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1", - intval($importer['uid']) - ); + require_once('bbcode.php'); - if((count($r)) && ($r[0]['notify-flags'] & NOTIFY_MAIL)) { + if($importer['notify-flags'] & NOTIFY_MAIL) { $tpl = file_get_contents('view/mail_received_eml.tpl'); $email_tpl = replace_macros($tpl, array( '$sitename' => $a->config['sitename'], '$siteurl' => $a->get_baseurl(), - '$username' => $r[0]['username'], - '$email' => $r[0]['email'], + '$username' => $importer['username'], + '$email' => $importer['email'], '$from' => $msg['from-name'], - '$fn' => $r[0]['name'], '$title' => $msg['title'], '$body' => strip_tags(bbcode($msg['body'])) )); - $res = mail($r[0]['email'], t("New mail received at ") . $a->config['sitename'], + $res = mail($importer['email'], t("New mail received at ") . $a->config['sitename'], $email_tpl,t("From: Administrator@") . $a->get_hostname() ); } xml_status(0); @@ -116,7 +113,7 @@ function dfrn_notify_post(&$a) { if($deleted) { $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($uri), - intval($importer['uid']) + intval($importer['importer_uid']) ); if(count($r)) { if($r[0]['uri'] == $r[0]['parent-uri']) { @@ -131,7 +128,7 @@ function dfrn_notify_post(&$a) { WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($when), dbesc($uri), - intval($importer['uid']) + intval($importer['importer_uid']) ); } } @@ -151,32 +148,48 @@ function dfrn_notify_post(&$a) { if($feed->get_item_quantity() == 1) { // remote reply to our post. Import and then notify everybody else. $datarray = get_atom_elements($item); - $urn = explode(':',$parent_urn); $datarray['type'] = 'remote-comment'; $datarray['parent-uri'] = $parent_uri; - $datarray['uid'] = $importer['uid']; + $datarray['uid'] = $importer['importer_uid']; $datarray['contact-id'] = $importer['id']; $posted_id = post_remote($a,$datarray); $r = q("SELECT `parent` FROM `item` WHERE `id` = %d AND `uid` = %d LIMIT 1", intval($posted_id), - intval($importer['uid']) + intval($importer['importer_uid']) ); if(count($r)) { $r1 = q("UPDATE `item` SET `last-child` = 0 WHERE `uid` = %d AND `parent` = %d", - intval($importer['uid']), + intval($importer['importer_uid']), intval($r[0]['parent']) ); } $r2 = q("UPDATE `item` SET `last-child` = 1 WHERE `uid` = %d AND `id` = %d LIMIT 1", - intval($importer['uid']), + intval($importer['importer_uid']), intval($posted_id) ); $url = $a->get_baseurl(); - proc_close(proc_open("php include/notifier.php $url comment-import $posted_id > remote-notify.log &", array(),$foo)); + proc_close(proc_open("php include/notifier.php $url comment-import $posted_id > remote-notify.log &", + array(),$foo)); + if(($importer['notify-flags'] & NOTIFY_COMMENT) && (! $importer['self'])) { + require_once('bbcode.php'); + $from = stripslashes($datarray['author-name']); + $tpl = file_get_contents('view/cmnt_received_eml.tpl'); + $email_tpl = replace_macros($tpl, array( + '$sitename' => $a->config['sitename'], + '$siteurl' => $a->get_baseurl(), + '$username' => $importer['username'], + '$email' => $importer['email'], + '$from' => $from, + '$body' => strip_tags(bbcode(stripslashes($datarray['body']))) + )); + + $res = mail($importer['email'], $from . t(" commented on your item at ") . $a->config['sitename'], + $email_tpl,t("From: Administrator@") . $a->get_hostname() ); + } xml_status(0); return; @@ -188,25 +201,56 @@ function dfrn_notify_post(&$a) { $r = q("SELECT `uid`, `last-child`, `edited` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($item_id), - intval($importer['uid']) + intval($importer['importer_uid']) ); // FIXME update content if 'updated' changes if(count($r)) { - $allow = $item->get_item_tags('http://purl.org/macgirvin/dfrn/1.0','comment-allow'); + $allow = $item->get_item_tags( NAMESPACE_DFRN, 'comment-allow'); if($allow && $allow[0]['data'] != $r[0]['last-child']) { $r = q("UPDATE `item` SET `last-child` = %d WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", intval($allow[0]['data']), dbesc($item_id), - intval($importer['uid']) + intval($importer['importer_uid']) ); } continue; } $datarray = get_atom_elements($item); $datarray['parent-uri'] = $parent_uri; - $datarray['uid'] = $importer['uid']; + $datarray['uid'] = $importer['importer_uid']; $datarray['contact-id'] = $importer['id']; $r = post_remote($a,$datarray); + + // find out if our user is involved in this conversation and wants to be notified. + + if($importer['notify-flags'] & NOTIFY_COMMENT) { + + $myconv = q("SELECT `author-link` FROM `item` WHERE `parent-uri` = '%s'", + dbesc($parent_uri) + ); + if(count($myconv)) { + foreach($myconv as $conv) { + if($conv['author-link'] != $importer['url']) + continue; + require_once('bbcode.php'); + $from = stripslashes($datarray['author-name']); + $tpl = file_get_contents('view/cmnt_received_eml.tpl'); + $email_tpl = replace_macros($tpl, array( + '$sitename' => $a->config['sitename'], + '$siteurl' => $a->get_baseurl(), + '$username' => $importer['username'], + '$email' => $importer['email'], + '$from' => $from, + '$body' => strip_tags(bbcode(stripslashes($datarray['body']))) + )); + + $res = mail($importer['email'], $from . t(" commented on an item at ") + . $a->config['sitename'], + $email_tpl,t("From: Administrator@") . $a->get_hostname() ); + break; + } + } + } continue; } } @@ -216,15 +260,15 @@ function dfrn_notify_post(&$a) { $item_id = $item->get_id(); $r = q("SELECT `uid`, `last-child`, `edited` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($item_id), - intval($importer['uid']) + intval($importer['importer_uid']) ); if(count($r)) { - $allow = $item->get_item_tags('http://purl.org/macgirvin/dfrn/1.0','comment-allow'); + $allow = $item->get_item_tags( NAMESPACE_DFRN, 'comment-allow'); if($allow && $allow[0]['data'] != $r[0]['last-child']) { $r = q("UPDATE `item` SET `last-child` = %d WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", intval($allow[0]['data']), dbesc($item_id), - intval($importer['uid']) + intval($importer['importer_uid']) ); } continue; @@ -233,7 +277,7 @@ function dfrn_notify_post(&$a) { $datarray = get_atom_elements($item); $datarray['parent-uri'] = $item_id; - $datarray['uid'] = $importer['uid']; + $datarray['uid'] = $importer['importer_uid']; $datarray['contact-id'] = $importer['id']; $r = post_remote($a,$datarray); continue; @@ -281,8 +325,7 @@ function dfrn_notify_content(&$a) { openssl_private_encrypt($id_str,$encrypted_id,$r[0]['prvkey']); $encrypted_id = bin2hex($encrypted_id); - echo '' .$status . '' . $encrypted_id . '' - . '' . $challenge . '' . "\r\n" ; + echo '' .$status . '' . $encrypted_id . '' . '' . $challenge . '' . "\r\n" ; session_write_close(); exit; diff --git a/mod/item.php b/mod/item.php index b718c68a66..e60b062bbc 100644 --- a/mod/item.php +++ b/mod/item.php @@ -27,11 +27,21 @@ function item_post(&$a) { $profile_uid = ((x($_POST,'profile_uid')) ? intval($_POST['profile_uid']) : 0); + if(! can_write_wall($a,$profile_uid)) { notice("Permission denied." . EOL) ; return; } + + $user = null; + + $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1", + intval($profile_uid) + ); + if(count($r)) + $user = $r[0]; + $str_group_allow = ''; $group_allow = $_POST['group_allow']; if(is_array($group_allow)) { @@ -71,23 +81,38 @@ function item_post(&$a) { // get contact info for poster - if((x($_SESSION,'visitor_id')) && (intval($_SESSION['visitor_id']))) { - $contact_id = $_SESSION['visitor_id']; + $author = null; + + if(($_SESSION['uid']) && ($_SESSION['uid'] == $profile_uid)) { + $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `self` = 1 LIMIT 1", + intval($_SESSION['uid']) + ); } else { - $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `self` = 1 LIMIT 1", - intval($_SESSION['uid'])); - if(count($r)) - $contact_id = $r[0]['id']; + if((x($_SESSION,'visitor_id')) && (intval($_SESSION['visitor_id']))) { + $r = q("SELECT * FROM `contact` WHERE `id` = %d LIMIT 1", + intval($_SESSION['visitor_id']) + ); + } + } + + if(count($r)) { + $author = $r[0]; + $contact_id = $author['id']; } // get contact info for owner - $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `self` = 1 LIMIT 1", - intval($profile_uid) - ); - if(count($r)) - $contact_record = $r[0]; + if($profile_uid == $_SESSION['uid']) { + $contact_record = $author; + } + else { + $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `self` = 1 LIMIT 1", + intval($profile_uid) + ); + if(count($r)) + $contact_record = $r[0]; + } $post_type = notags(trim($_POST['type'])); @@ -115,15 +140,19 @@ function item_post(&$a) { } while($dups == true); - $r = q("INSERT INTO `item` (`uid`,`type`,`contact-id`,`owner-name`,`owner-link`,`owner-avatar`, `created`, + $r = q("INSERT INTO `item` (`uid`,`type`,`contact-id`,`owner-name`,`owner-link`,`owner-avatar`, + `author-name`, `author-link`, `author-avatar`, `created`, `edited`, `uri`, `title`, `body`, `allow_cid`, `allow_gid`, `deny_cid`, `deny_gid`) - VALUES( %d, '%s', %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )", + VALUES( %d, '%s', %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )", intval($profile_uid), dbesc($post_type), intval($contact_id), dbesc($contact_record['name']), dbesc($contact_record['url']), dbesc($contact_record['thumb']), + dbesc($author['name']), + dbesc($author['url']), + dbesc($author['thumb']), datetime_convert(), datetime_convert(), dbesc($uri), @@ -157,9 +186,43 @@ function item_post(&$a) { dbesc($parent_item['deny_gid']), intval($post_id) ); + + if(($user['notify-flags'] & NOTIFY_COMMENT) && ($contact_record != $author)) { + require_once('bbcode.php'); + $from = $author['name']; + $tpl = file_get_contents('view/cmnt_received_eml.tpl'); + $email_tpl = replace_macros($tpl, array( + '$sitename' => $a->config['sitename'], + '$siteurl' => $a->get_baseurl(), + '$username' => $user['username'], + '$email' => $user['email'], + '$from' => $from, + '$body' => strip_tags(bbcode($body)) + )); + + $res = mail($user['email'], $from . t(" commented on your item at ") . $a->config['sitename'], + $email_tpl,t("From: Administrator@") . $a->get_hostname() ); + } } else { $parent = $post_id; + + if(($user['notify-flags'] & NOTIFY_WALL) && ($contact_record != $author)) { + require_once('bbcode.php'); + $from = $author['name']; + $tpl = file_get_contents('view/wall_received_eml.tpl'); + $email_tpl = replace_macros($tpl, array( + '$sitename' => $a->config['sitename'], + '$siteurl' => $a->get_baseurl(), + '$username' => $user['username'], + '$email' => $user['email'], + '$from' => $from, + '$body' => strip_tags(bbcode($body)) + )); + + $res = mail($user['email'], $from . t(" posted on your profile wall at ") . $a->config['sitename'], + $email_tpl,t("From: Administrator@") . $a->get_hostname() ); + } } $r = q("UPDATE `item` SET `parent` = %d, `parent-uri` = '%s', `last-child` = 1, `visible` = 1 @@ -202,7 +265,7 @@ function item_content(&$a) { ); if(! count($r)) { - notice("Item not found." . EOL); + notice( t('Item not found.') . EOL); goaway($a->get_baseurl() . '/' . $_SESSION['return_url']); } $item = $r[0]; @@ -255,7 +318,7 @@ function item_content(&$a) { return; //NOTREACHED } else { - notice("Permission denied." . EOL); + notice( t('Permission denied.') . EOL); goaway($a->get_baseurl() . '/' . $_SESSION['return_url']); return; //NOTREACHED } diff --git a/mod/profile.php b/mod/profile.php index 2356f913e3..1b37d31bc0 100644 --- a/mod/profile.php +++ b/mod/profile.php @@ -91,9 +91,16 @@ function profile_content(&$a, $update = false) { if(remote_user()) { $contact_id = $_SESSION['visitor_id']; $groups = init_groups_visitor($contact_id); + $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1", + intval($contact_id), + intval($a->profile['uid']) + ); + if(count($r)) + $contact = $r[0]; } if(local_user()) { $contact_id = $_SESSION['cid']; + $contact = $a->contact; } if($update) { @@ -239,9 +246,9 @@ function profile_content(&$a, $update = false) { '$id' => $item['item_id'], '$parent' => $item['parent'], '$profile_uid' => $a->profile['uid'], - '$mylink' => $a->contact['url'], + '$mylink' => $contact['url'], '$mytitle' => t('Me'), - '$myphoto' => $a->contact['thumb'], + '$myphoto' => $contact['thumb'], '$ww' => '' )); } diff --git a/mod/settings.php b/mod/settings.php index 252e0efe32..10336e7716 100644 --- a/mod/settings.php +++ b/mod/settings.php @@ -56,6 +56,19 @@ function settings_post(&$a) { $email = notags(trim($_POST['email'])); $timezone = notags(trim($_POST['timezone'])); + $notify = 0; + + if($_POST['notify1']) + $notify += intval($_POST['notify1']); + if($_POST['notify2']) + $notify += intval($_POST['notify2']); + if($_POST['notify3']) + $notify += intval($_POST['notify3']); + if($_POST['notify4']) + $notify += intval($_POST['notify4']); + if($_POST['notify5']) + $notify += intval($_POST['notify5']); + $username_changed = false; $email_changed = false; $zone_changed = false; @@ -120,7 +133,7 @@ function settings_post(&$a) { - $r = q("UPDATE `user` SET `username` = '%s', `email` = '%s', `timezone` = '%s', `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s' WHERE `uid` = %d LIMIT 1", + $r = q("UPDATE `user` SET `username` = '%s', `email` = '%s', `timezone` = '%s', `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s', `notify-flags` = %d WHERE `uid` = %d LIMIT 1", dbesc($username), dbesc($email), dbesc($timezone), @@ -128,6 +141,7 @@ function settings_post(&$a) { dbesc($str_group_allow), dbesc($str_contact_deny), dbesc($str_group_deny), + intval($notify), intval($_SESSION['uid']) ); if($r) @@ -163,7 +177,7 @@ function settings_content(&$a) { $email = $a->user['email']; $nickname = $a->user['nickname']; $timezone = $a->user['timezone']; - + $notify = $a->user['notify-flags']; $nickname_block = file_get_contents("view/settings_nick_set.tpl"); @@ -198,7 +212,12 @@ function settings_content(&$a) { '$timezone' => $timezone, '$zoneselect' => select_timezone($timezone), '$permissions' => t('Default Post Permissions'), - '$aclselect' => populate_acl($a->user) + '$aclselect' => populate_acl($a->user), + '$sel_notify1' => (($notify & NOTIFY_INTRO) ? ' checked="checked" ' : ''), + '$sel_notify2' => (($notify & NOTIFY_CONFIRM) ? ' checked="checked" ' : ''), + '$sel_notify3' => (($notify & NOTIFY_WALL) ? ' checked="checked" ' : ''), + '$sel_notify4' => (($notify & NOTIFY_COMMENT) ? ' checked="checked" ' : ''), + '$sel_notify5' => (($notify & NOTIFY_MAIL) ? ' checked="checked" ' : '') )); return $o; diff --git a/view/cmnt_received_eml.tpl b/view/cmnt_received_eml.tpl new file mode 100644 index 0000000000..8205831aaf --- /dev/null +++ b/view/cmnt_received_eml.tpl @@ -0,0 +1,16 @@ + +Dear $username, + + '$from' commented on an item/conversation which you have been following. + +----- +$body +----- + +Please login at $siteurl to view the complete conversation. + +Thank you, + $sitename administrator + + + diff --git a/view/settings.tpl b/view/settings.tpl index 658c0b6850..a918aefc1a 100644 --- a/view/settings.tpl +++ b/view/settings.tpl @@ -37,7 +37,25 @@ $zoneselect
- +
+
Send me a notification email when:
+ + +
+ + +
+ + +
+ + +
+ + +
+
+

diff --git a/view/style.css b/view/style.css index 91a4d32324..788a183499 100644 --- a/view/style.css +++ b/view/style.css @@ -298,21 +298,55 @@ input#dfrn-url { margin-bottom: 30px; } -#settings-username-end, #settings-email-end, #settings-nick-end, #settings-timezone-end, #settings-password-end, #settings-confirm-end { +#settings-username-end, +#settings-email-end, +#settings-nick-end, +#settings-timezone-end, +#settings-password-end, +#settings-confirm-end, +#notify1-end, +#notify2-end, +#notify3-end, +#notify4-end, +#notify5-end { margin-bottom: 5px; clear: both; } -#settings-username-label, #settings-email-label, #settings-nick-label, #settings-timezone-label, #settings-password-label, #settings-confirm-label { +#settings-username-label, +#settings-email-label, +#settings-nick-label, +#settings-timezone-label, +#settings-password-label, +#settings-confirm-label, +#settings-label-notify1, +#settings-label-notify2, +#settings-label-notify3, +#settings-label-notify4, +#settings-label-notify5 { float: left; width: 200px; } -#settings-username, #settings-email, #settings-nick, #timezone-select, #settings-password, #settings-confirm { +#settings-username, +#settings-email, +#settings-nick, +#timezone-select, +#settings-password, +#settings-confirm, +#notify1, +#notify2, +#notify3, +#notify4, +#notify5 { float: left; margin-bottom: 20px; } +#settings-notify-desc { + margin-top: 20px; + margin-bottom: 20px; +} #settings-nick-desc { width: 500px; } diff --git a/view/wall_received_eml.tpl b/view/wall_received_eml.tpl new file mode 100644 index 0000000000..cd3e18a5c7 --- /dev/null +++ b/view/wall_received_eml.tpl @@ -0,0 +1,16 @@ + +Dear $username, + + '$from' posted something to your profile wall. + +----- +$body +----- + +Please login at $siteurl to view or delete the item. + +Thank you, + $sitename administrator + + +