diff options
Diffstat (limited to 'include')
-rw-r--r-- | include/Contact.php | 82 | ||||
-rw-r--r-- | include/acl_selectors.php | 2 | ||||
-rw-r--r-- | include/api.php | 53 | ||||
-rw-r--r-- | include/apps.php | 8 | ||||
-rw-r--r-- | include/auth.php | 37 | ||||
-rw-r--r-- | include/bbcode.php | 19 | ||||
-rw-r--r-- | include/chat.php | 6 | ||||
-rw-r--r-- | include/contact_selectors.php | 4 | ||||
-rw-r--r-- | include/contact_widgets.php | 1 | ||||
-rw-r--r-- | include/conversation.php | 9 | ||||
-rw-r--r-- | include/crypto.php | 192 | ||||
-rwxr-xr-x | include/diaspora.php | 2691 | ||||
-rw-r--r-- | include/dir_fns.php | 31 | ||||
-rw-r--r-- | include/enotify.php | 16 | ||||
-rw-r--r-- | include/externals.php | 24 | ||||
-rw-r--r-- | include/features.php | 2 | ||||
-rw-r--r-- | include/identity.php | 78 | ||||
-rwxr-xr-x | include/items.php | 99 | ||||
-rw-r--r-- | include/nav.php | 7 | ||||
-rw-r--r-- | include/permissions.php | 80 | ||||
-rw-r--r-- | include/photo/photo_driver.php | 22 | ||||
-rw-r--r-- | include/photos.php | 88 | ||||
-rw-r--r-- | include/profile_selectors.php | 55 | ||||
-rw-r--r-- | include/widgets.php | 7 | ||||
-rw-r--r-- | include/zot.php | 85 |
25 files changed, 3495 insertions, 203 deletions
diff --git a/include/Contact.php b/include/Contact.php index 787612c83..140f449af 100644 --- a/include/Contact.php +++ b/include/Contact.php @@ -162,7 +162,7 @@ function user_remove($uid) { } -function account_remove($account_id,$local = true) { +function account_remove($account_id,$local = true,$unset_session=true) { logger('account_remove: ' . $account_id); @@ -185,6 +185,7 @@ function account_remove($account_id,$local = true) { $r = q("select * from account where account_id = %d limit 1", intval($account_id) ); + $account_email=$r[0]['account_email']; if(! $r) { logger('account_remove: No account with id: ' . $account_id); @@ -196,7 +197,7 @@ function account_remove($account_id,$local = true) { ); if($x) { foreach($x as $xx) { - channel_remove($xx['channel_id'],$local); + channel_remove($xx['channel_id'],$local,false); } } @@ -204,11 +205,17 @@ function account_remove($account_id,$local = true) { intval($account_id) ); + if ($unset_session) { + unset($_SESSION['authenticated']); + unset($_SESSION['uid']); + notice( sprintf(t("User '%s' deleted"),$account_email) . EOL); + goaway(get_app()->get_baseurl()); + } return $r; } -function channel_remove($channel_id, $local = true) { +function channel_remove($channel_id, $local = true, $unset_session=true) { if(! $channel_id) return; @@ -292,7 +299,7 @@ function channel_remove($channel_id, $local = true) { proc_run('php','include/directory.php',$channel_id); - if($channel_id == local_user()) { + if($channel_id == local_user() && $unset_session) { unset($_SESSION['authenticated']); unset($_SESSION['uid']); goaway($a->get_baseurl()); @@ -506,73 +513,6 @@ function contact_remove($channel_id, $abook_id) { } -// sends an unfriend message. Does not remove the contact - -function terminate_friendship($user,$self,$contact) { - - - $a = get_app(); - - require_once('include/datetime.php'); - - if($contact['network'] === NETWORK_DFRN) { - require_once('include/items.php'); - dfrn_deliver($user,$contact,'placeholder', 1); - } - -} - - -// Contact has refused to recognise us as a friend. We will start a countdown. -// If they still don't recognise us in 32 days, the relationship is over, -// and we won't waste any more time trying to communicate with them. -// This provides for the possibility that their database is temporarily messed -// up or some other transient event and that there's a possibility we could recover from it. - -if(! function_exists('mark_for_death')) { -function mark_for_death($contact) { - - if($contact['archive']) - return; - - if($contact['term_date'] == '0000-00-00 00:00:00') { - q("UPDATE `contact` SET `term_date` = '%s' WHERE `id` = %d LIMIT 1", - dbesc(datetime_convert()), - intval($contact['id']) - ); - } - else { - - // TODO: We really should send a notification to the owner after 2-3 weeks - // so they won't be surprised when the contact vanishes and can take - // remedial action if this was a serious mistake or glitch - - $expiry = $contact['term_date'] . ' + 32 days '; - if(datetime_convert() > datetime_convert('UTC','UTC',$expiry)) { - - // relationship is really truly dead. - // archive them rather than delete - // though if the owner tries to unarchive them we'll start the whole process over again - - q("update contact set `archive` = 1 where id = %d limit 1", - intval($contact['id']) - ); - - //contact_remove($contact['id']); - - } - } - -}} - -if(! function_exists('unmark_for_death')) { -function unmark_for_death($contact) { - // It's a miracle. Our dead contact has inexplicably come back to life. - q("UPDATE `contact` SET `term_date` = '%s' WHERE `id` = %d LIMIT 1", - dbesc('0000-00-00 00:00:00'), - intval($contact['id']) - ); -}} function random_profile() { $r = q("select xchan_url from xchan left join hubloc on hubloc_hash = xchan_hash where hubloc_connected > UTC_TIMESTAMP() - interval 30 day order by rand() limit 1"); diff --git a/include/acl_selectors.php b/include/acl_selectors.php index 8d94264e4..0b68ba227 100644 --- a/include/acl_selectors.php +++ b/include/acl_selectors.php @@ -237,7 +237,7 @@ function populate_acl($defaults = null,$show_jotnets = true) { $tpl = get_markup_template("acl_selector.tpl"); $o = replace_macros($tpl, array( - '$showall'=> t("Visible to everybody"), + '$showall'=> t("Visible to your default audience"), '$show' => t("Show"), '$hide' => t("Don't show"), '$allowcid' => json_encode($allow_cid), diff --git a/include/api.php b/include/api.php index 57551a3b0..c0f54af19 100644 --- a/include/api.php +++ b/include/api.php @@ -119,16 +119,36 @@ require_once('include/items.php'); // process normal login request require_once('include/auth.php'); + $channel_login = 0; $record = account_verify_password($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']); if(! $record) { - logger('API_login failure: ' . print_r($_SERVER,true), LOGGER_DEBUG); - header('WWW-Authenticate: Basic realm="Red"'); - header('HTTP/1.0 401 Unauthorized'); - die('This api requires login'); + $r = q("select * from channel where channel_address = '%s' limit 1", + dbesc($_SERVER['PHP_AUTH_USER']) + ); + if ($r) { + $x = q("select * from account where account_id = %d limit 1", + intval($r[0]['channel_account_id']) + ); + if ($x) { + $record = account_verify_password($x[0]['account_email'],$_SERVER['PHP_AUTH_PW']); + if($record) + $channel_login = $r[0]['channel_id']; + } + } + if(! $record) { + logger('API_login failure: ' . print_r($_SERVER,true), LOGGER_DEBUG); + header('WWW-Authenticate: Basic realm="Red"'); + header('HTTP/1.0 401 Unauthorized'); + die('This api requires login'); + } } require_once('include/security.php'); authenticate_success($record); + + if($channel_login) + change_channel($channel_login); + $_SESSION['allow_api'] = true; } @@ -1498,6 +1518,9 @@ require_once('include/items.php'); $a = get_app(); $ret = array(); + if(! $r) + return $ret; + foreach($r as $item) { localize_item($item); @@ -1875,17 +1898,19 @@ require_once('include/items.php'); ); $ret = Array(); - foreach($r as $item) { - if ($box == "inbox" || $item['from-url'] != $profile_url){ - $recipient = $user_info; - $sender = api_get_user($a,$item['contact-id']); - } - elseif ($box == "sentbox" || $item['from-url'] != $profile_url){ - $recipient = api_get_user($a,$item['contact-id']); - $sender = $user_info; + if($r) { + foreach($r as $item) { + if ($box == "inbox" || $item['from-url'] != $profile_url){ + $recipient = $user_info; + $sender = api_get_user($a,$item['contact-id']); + } + elseif ($box == "sentbox" || $item['from-url'] != $profile_url){ + $recipient = api_get_user($a,$item['contact-id']); + $sender = $user_info; + } + + $ret[]=api_format_messages($item, $recipient, $sender); } - - $ret[]=api_format_messages($item, $recipient, $sender); } diff --git a/include/apps.php b/include/apps.php index 135eaa99a..bd50b953a 100644 --- a/include/apps.php +++ b/include/apps.php @@ -141,7 +141,13 @@ function translate_system_apps(&$arr) { 'Chat' => t('Chat'), 'Search' => t('Search'), 'Probe' => t('Probe'), - 'Suggest' => t('Suggest') + 'Suggest' => t('Suggest'), + 'Random Channel' => t('Random Channel'), + 'Invite' => t('Invite'), + 'Features' => t('Features'), + 'Language' => t('Language'), + 'Post' => t('Post'), + 'Profile Photo' => t('Profile Photo') ); if(array_key_exists($arr['name'],$apps)) diff --git a/include/auth.php b/include/auth.php index 8e02b7b4f..cc07917b7 100644 --- a/include/auth.php +++ b/include/auth.php @@ -128,13 +128,40 @@ if((isset($_SESSION)) && (x($_SESSION,'authenticated')) && ((! (x($_POST,'auth-p // first check if we're enforcing that sessions can't change IP address - if($_SESSION['addr'] != $_SERVER['REMOTE_ADDR']) { + if($_SESSION['addr'] && $_SESSION['addr'] != $_SERVER['REMOTE_ADDR']) { logger('SECURITY: Session IP address changed: ' . $_SESSION['addr'] . ' != ' . $_SERVER['REMOTE_ADDR']); - if(get_config('system','paranoia')) { - logger('Session address changed. Paranoid setting in effect, blocking session. ' + + $partial1 = substr($_SESSION['addr'],0,strrpos($_SESSION['addr'],'.')); + $partial2 = substr($_SERVER['REMOTE_ADDR'],0,strrpos($_SERVER['REMOTE_ADDR'],'.')); + + + $paranoia = intval(get_pconfig($_SESSION['uid'],'system','paranoia')); + if(! $paranoia) + $paranoia = intval(get_config('system','paranoia')); + + switch($paranoia) { + case 0: + // no IP checking + break; + case 2: + // check 2 octets + $partial1 = substr($partial1,0,strrpos($partial1,'.')); + $partial2 = substr($partial2,0,strrpos($partial2,'.')); + if($partial1 == $partial2) + break; + case 1: + // check 3 octets + if($partial1 == $partial2) + break; + case 3: + default: + // check any difference at all + logger('Session address changed. Paranoid setting in effect, blocking session. ' . $_SESSION['addr'] . ' != ' . $_SERVER['REMOTE_ADDR']); - nuke_session(); - goaway(z_root()); + nuke_session(); + goaway(z_root()); + break; + } } diff --git a/include/bbcode.php b/include/bbcode.php index 45126c0eb..a7055fc45 100644 --- a/include/bbcode.php +++ b/include/bbcode.php @@ -502,6 +502,13 @@ function bbcode($Text,$preserve_nl = false, $tryoembed = true) { if (strpos($Text,'[o]') !== false) { $Text = preg_replace("(\[o\](.*?)\[\/o\])ism",'<span class="overline">$1</span>',$Text); } + if (strpos($Text,'[sup]') !== false) { + $Text = preg_replace("(\[sup\](.*?)\[\/sup\])ism",'<sup>$1</sup>',$Text); + } + if (strpos($Text,'[sub]') !== false) { + $Text = preg_replace("(\[sub\](.*?)\[\/sub\])ism",'<sub>$1</sub>',$Text); + } + // Check for colored text if (strpos($Text,'[/color]') !== false) { $Text = preg_replace("(\[color=(.*?)\](.*?)\[\/color\])ism","<span style=\"color: $1;\">$2</span>",$Text); @@ -657,24 +664,24 @@ function bbcode($Text,$preserve_nl = false, $tryoembed = true) { // [img=widthxheight]pathtoimage[/img] if (strpos($Text,'[/img]') !== false) { - $Text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '<img src="$3" style="width: 100%; max-width: $1px;" alt="' . t('Image/photo') . '" >', $Text); + $Text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '<img src="$3" style="width: 100%; max-width: $1px;" alt="' . t('Image/photo') . '" />', $Text); } if (strpos($Text,'[/zmg]') !== false) { - $Text = preg_replace("/\[zmg\=([0-9]*)x([0-9]*)\](.*?)\[\/zmg\]/ism", '<img class="zrl" src="$3" style="width: 100%; max-width: $1px;" alt="' . t('Image/photo') . '" >', $Text); + $Text = preg_replace("/\[zmg\=([0-9]*)x([0-9]*)\](.*?)\[\/zmg\]/ism", '<img class="zrl" src="$3" style="width: 100%; max-width: $1px;" alt="' . t('Image/photo') . '" />', $Text); } // [img=widthxheight float={left, right}]pathtoimage[/img] if (strpos($Text,'[/img]') !== false) { - $Text = preg_replace("/\[img\=([0-9]*)x([0-9]*) float=left\](.*?)\[\/img\]/ism", '<img src="$3" style="width: 100%; max-width: $1px; float: left;" alt="' . t('Image/photo') . '" >', $Text); + $Text = preg_replace("/\[img\=([0-9]*)x([0-9]*) float=left\](.*?)\[\/img\]/ism", '<img src="$3" style="width: 100%; max-width: $1px; float: left;" alt="' . t('Image/photo') . '" />', $Text); } if (strpos($Text,'[/img]') !== false) { - $Text = preg_replace("/\[img\=([0-9]*)x([0-9]*) float=right\](.*?)\[\/img\]/ism", '<img src="$3" style="width: 100%; max-width: $1px; float: right;" alt="' . t('Image/photo') . '" >', $Text); + $Text = preg_replace("/\[img\=([0-9]*)x([0-9]*) float=right\](.*?)\[\/img\]/ism", '<img src="$3" style="width: 100%; max-width: $1px; float: right;" alt="' . t('Image/photo') . '" />', $Text); } if (strpos($Text,'[/zmg]') !== false) { - $Text = preg_replace("/\[zmg\=([0-9]*)x([0-9]*) float=left\](.*?)\[\/zmg\]/ism", '<img class="zrl" src="$3" style="width: 100%; max-width: $1px; float: left;" alt="' . t('Image/photo') . '" >', $Text); + $Text = preg_replace("/\[zmg\=([0-9]*)x([0-9]*) float=left\](.*?)\[\/zmg\]/ism", '<img class="zrl" src="$3" style="width: 100%; max-width: $1px; float: left;" alt="' . t('Image/photo') . '" />', $Text); } if (strpos($Text,'[/zmg]') !== false) { - $Text = preg_replace("/\[zmg\=([0-9]*)x([0-9]*) float=right\](.*?)\[\/zmg\]/ism", '<img class="zrl" src="$3" style="width: 100%; max-width: $1px; float: right;" alt="' . t('Image/photo') . '" >', $Text); + $Text = preg_replace("/\[zmg\=([0-9]*)x([0-9]*) float=right\](.*?)\[\/zmg\]/ism", '<img class="zrl" src="$3" style="width: 100%; max-width: $1px; float: right;" alt="' . t('Image/photo') . '" />', $Text); } // style (sanitized) diff --git a/include/chat.php b/include/chat.php index 9d2341dfa..5f69853e7 100644 --- a/include/chat.php +++ b/include/chat.php @@ -119,10 +119,10 @@ function chatroom_enter($observer_xchan,$room_id,$status,$client) { $limit = service_class_fetch($r[0]['cr_uid'],'chatters_inroom'); if($limit !== false) { - $x = q("select count(*) as total from chatpresence where cp_room = %d", + $y = q("select count(*) as total from chatpresence where cp_room = %d", intval($room_id) ); - if($x && $x[0]['total'] > $limit) { + if($y && $y[0]['total'] > $limit) { notice( t('Room is full') . EOL); return false; } @@ -235,6 +235,8 @@ function chat_message($uid,$room_id,$xchan,$text) { */ function chatroom_flush($room_id,$xchan) { + + $date_limit = date('Y-m-d H:i:s', time() - 3600 * MAX_CHATROOM_HOURS); $d = q("delete from chat where chat_room = %d and chat_xchan = '%s' and created < '%s'", intval($room_id), diff --git a/include/contact_selectors.php b/include/contact_selectors.php index a3cfd2489..726efce9d 100644 --- a/include/contact_selectors.php +++ b/include/contact_selectors.php @@ -20,6 +20,7 @@ function contact_profile_assign($current) { return $o; } +/* unused currently function contact_reputation($current) { @@ -27,7 +28,7 @@ function contact_reputation($current) { $o .= "<select id=\"contact-reputation-selector\" name=\"reputation\" />\r\n"; $rep = array( - 0 => t('Unknown | Not categorised'), + 0 => t('Unknown | Not categorized'), 1 => t('Block immediately'), 2 => t('Shady, spammer, self-marketer'), 3 => t('Known to me, but no opinion'), @@ -43,6 +44,7 @@ function contact_reputation($current) { return $o; } +*/ function contact_poll_interval($current, $disabled = false) { diff --git a/include/contact_widgets.php b/include/contact_widgets.php index 758b7291b..28a9fcfd3 100644 --- a/include/contact_widgets.php +++ b/include/contact_widgets.php @@ -79,6 +79,7 @@ function categories_widget($baseurl,$selected = '') { and term.uid = item.uid and term.type = %d and item.author_xchan = '%s' + and item.item_restrict = 0 order by term.term asc", intval($a->profile['profile_uid']), intval(TERM_CATEGORY), diff --git a/include/conversation.php b/include/conversation.php index 5481037e7..836bd1b24 100644 --- a/include/conversation.php +++ b/include/conversation.php @@ -179,8 +179,8 @@ function localize_item(&$item){ if (activity_match($item['verb'],ACTIVITY_FRIEND)) { - -// if ($item['obj_type']=="" || $item['obj_type']!== ACTIVITY_OBJ_PERSON) return; + if ($item['obj_type'] == "" || $item['obj_type'] !== ACTIVITY_OBJ_PERSON) + return; $Aname = $item['author']['xchan_name']; $Alink = $item['author']['xchan_url']; @@ -902,6 +902,7 @@ function item_photo_menu($item){ $contact_url=""; $pm_url=""; $vsrc_link = ""; + $follow_url = ""; if(local_user()) { $ssl_state = true; @@ -923,6 +924,9 @@ function item_photo_menu($item){ if($a->contacts && array_key_exists($item['author_xchan'],$a->contacts)) $contact = $a->contacts[$item['author_xchan']]; + else + if(local_user() && $item['author']['xchan_addr']) + $follow_url = z_root() . '/follow/?f=&url=' . $item['author']['xchan_addr']; if($contact) { $poke_link = $a->get_baseurl($ssl_state) . '/poke/?f=&c=' . $contact['abook_id']; @@ -940,6 +944,7 @@ function item_photo_menu($item){ t("View Profile") => $profile_link, t("View Photos") => $photos_link, t("Matrix Activity") => $posts_link, + t("Follow") => $follow_url, t("Edit Contact") => $contact_url, t("Send PM") => $pm_url, t("Poke") => $poke_link diff --git a/include/crypto.php b/include/crypto.php index 33cdc10c0..c053dfae2 100644 --- a/include/crypto.php +++ b/include/crypto.php @@ -127,3 +127,195 @@ function new_keypair($bits) { } +function pkcs1to8($oldkey,$len) { + + if($len == 4096) + $c = 'g'; + if($len == 2048) + $c = 'Q'; + + if(strstr($oldkey,'BEGIN PUBLIC')) + return $oldkey; + + $oldkey = str_replace('-----BEGIN RSA PUBLIC KEY-----', '', $oldkey); + $oldkey = trim(str_replace('-----END RSA PUBLIC KEY-----', '', $oldkey)); + $key = 'MIICIjANBgkqhkiG9w0BAQEFAAOCA' . $c . '8A' . str_replace("\n", '', $oldkey); + $key = "-----BEGIN PUBLIC KEY-----\n" . wordwrap($key, 64, "\n", true) . "\n-----END PUBLIC KEY-----"; + return $key; +} + +function pkcs8to1($oldkey,$len) { + + if(strstr($oldkey,'BEGIN RSA')) + return $oldkey; + + $oldkey = str_replace('-----BEGIN PUBLIC KEY-----', '', $oldkey); + $oldkey = trim(str_replace('-----END PUBLIC KEY-----', '', $oldkey)); + $key = str_replace("\n",'',$oldkey); + $key = substr($key,32); + $key = "-----BEGIN RSA PUBLIC KEY-----\n" . wordwrap($key, 64, "\n", true) . "\n-----END RSA PUBLIC KEY-----"; + return $key; +} + + +function DerToPem($Der, $Private=false) +{ + //Encode: + $Der = base64_encode($Der); + //Split lines: + $lines = str_split($Der, 65); + $body = implode("\n", $lines); + //Get title: + $title = $Private? 'RSA PRIVATE KEY' : 'PUBLIC KEY'; + //Add wrapping: + $result = "-----BEGIN {$title}-----\n"; + $result .= $body . "\n"; + $result .= "-----END {$title}-----\n"; + + return $result; +} + +function DerToRsa($Der) +{ + //Encode: + $Der = base64_encode($Der); + //Split lines: + $lines = str_split($Der, 64); + $body = implode("\n", $lines); + //Get title: + $title = 'RSA PUBLIC KEY'; + //Add wrapping: + $result = "-----BEGIN {$title}-----\n"; + $result .= $body . "\n"; + $result .= "-----END {$title}-----\n"; + + return $result; +} + + +function pkcs8_encode($Modulus,$PublicExponent) { + //Encode key sequence + $modulus = new ASNValue(ASNValue::TAG_INTEGER); + $modulus->SetIntBuffer($Modulus); + $publicExponent = new ASNValue(ASNValue::TAG_INTEGER); + $publicExponent->SetIntBuffer($PublicExponent); + $keySequenceItems = array($modulus, $publicExponent); + $keySequence = new ASNValue(ASNValue::TAG_SEQUENCE); + $keySequence->SetSequence($keySequenceItems); + //Encode bit string + $bitStringValue = $keySequence->Encode(); + $bitStringValue = chr(0x00) . $bitStringValue; //Add unused bits byte + $bitString = new ASNValue(ASNValue::TAG_BITSTRING); + $bitString->Value = $bitStringValue; + //Encode body + $bodyValue = "\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05\x00" . $bitString->Encode(); + $body = new ASNValue(ASNValue::TAG_SEQUENCE); + $body->Value = $bodyValue; + //Get DER encoded public key: + $PublicDER = $body->Encode(); + return $PublicDER; +} + + +function pkcs1_encode($Modulus,$PublicExponent) { + //Encode key sequence + $modulus = new ASNValue(ASNValue::TAG_INTEGER); + $modulus->SetIntBuffer($Modulus); + $publicExponent = new ASNValue(ASNValue::TAG_INTEGER); + $publicExponent->SetIntBuffer($PublicExponent); + $keySequenceItems = array($modulus, $publicExponent); + $keySequence = new ASNValue(ASNValue::TAG_SEQUENCE); + $keySequence->SetSequence($keySequenceItems); + //Encode bit string + $bitStringValue = $keySequence->Encode(); + return $bitStringValue; +} + + +function metopem($m,$e) { + $der = pkcs8_encode($m,$e); + $key = DerToPem($der,false); + return $key; +} + + +function pubrsatome($key,&$m,&$e) { + require_once('library/asn1.php'); + require_once('include/salmon.php'); + + $lines = explode("\n",$key); + unset($lines[0]); + unset($lines[count($lines)]); + $x = base64_decode(implode('',$lines)); + + $r = ASN_BASE::parseASNString($x); + + $m = base64url_decode($r[0]->asnData[0]->asnData); + $e = base64url_decode($r[0]->asnData[1]->asnData); +} + + +function rsatopem($key) { + pubrsatome($key,$m,$e); + return(metopem($m,$e)); +} + +function pemtorsa($key) { + pemtome($key,$m,$e); + return(metorsa($m,$e)); +} + +function pemtome($key,&$m,&$e) { + require_once('include/salmon.php'); + $lines = explode("\n",$key); + unset($lines[0]); + unset($lines[count($lines)]); + $x = base64_decode(implode('',$lines)); + + $r = ASN_BASE::parseASNString($x); + + $m = base64url_decode($r[0]->asnData[1]->asnData[0]->asnData[0]->asnData); + $e = base64url_decode($r[0]->asnData[1]->asnData[0]->asnData[1]->asnData); +} + +function metorsa($m,$e) { + $der = pkcs1_encode($m,$e); + $key = DerToRsa($der); + return $key; +} + +function salmon_key($pubkey) { + pemtome($pubkey,$m,$e); + return 'RSA' . '.' . base64url_encode($m,true) . '.' . base64url_encode($e,true) ; +} + +// old function for providing mysql compatible encryption and is also +// used in Friendica 'RINO'. This function is messy and should be retired. + + +if(! function_exists('aes_decrypt')) { +function aes_decrypt($val,$ky) +{ + $key="\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + for($a=0;$a<strlen($ky);$a++) + $key[$a%16]=chr(ord($key[$a%16]) ^ ord($ky[$a])); + $mode = MCRYPT_MODE_ECB; + $enc = MCRYPT_RIJNDAEL_128; + $dec = @mcrypt_decrypt($enc, $key, $val, $mode, @mcrypt_create_iv( @mcrypt_get_iv_size($enc, $mode), MCRYPT_DEV_URANDOM ) ); + return rtrim($dec,(( ord(substr($dec,strlen($dec)-1,1))>=0 and ord(substr($dec, strlen($dec)-1,1))<=16)? chr(ord( substr($dec,strlen($dec)-1,1))):null)); +}} + + +if(! function_exists('aes_encrypt')) { +function aes_encrypt($val,$ky) +{ + $key="\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + for($a=0;$a<strlen($ky);$a++) + $key[$a%16]=chr(ord($key[$a%16]) ^ ord($ky[$a])); + $mode=MCRYPT_MODE_ECB; + $enc=MCRYPT_RIJNDAEL_128; + $val=str_pad($val, (16*(floor(strlen($val) / 16)+(strlen($val) % 16==0?2:1))), chr(16-(strlen($val) % 16))); + return mcrypt_encrypt($enc, $key, $val, $mode, mcrypt_create_iv( mcrypt_get_iv_size($enc, $mode), MCRYPT_DEV_URANDOM)); +}} + + diff --git a/include/diaspora.php b/include/diaspora.php new file mode 100755 index 000000000..6121466f2 --- /dev/null +++ b/include/diaspora.php @@ -0,0 +1,2691 @@ +<?php + +require_once('include/crypto.php'); +require_once('include/items.php'); +require_once('include/bb2diaspora.php'); +require_once('include/contact_selectors.php'); +//require_once('include/queue_fn.php'); +//require_once('include/lock.php'); + +function diaspora_dispatch_public($msg) { + + $enabled = intval(get_config('system','diaspora_enabled')); + if(! $enabled) { + logger('mod-diaspora: disabled'); + return; + } + + // find everybody following or allowing this author + + + $r = q("SELECT * from channel where channel_id in ( SELECT abook_channel from abook WHERE abook_network = 'diaspora' and abook_xchan = '%s' )", + dbesc($msg['author']) + ); + + // also need to look for those following public streams + + if($r) { + foreach($r as $rr) { + logger('diaspora_public: delivering to: ' . $rr['channel_name'] . ' (' . $rr['channel_address'] . ') '); + diaspora_dispatch($rr,$msg); + } + } + else + logger('diaspora_public: no subscribers'); +} + + + +function diaspora_dispatch($importer,$msg,$attempt=1) { + + $ret = 0; + + $enabled = intval(get_config('system','diaspora_enabled')); + if(! $enabled) { + logger('mod-diaspora: disabled'); + return; + } + + // php doesn't like dashes in variable names + + $msg['message'] = str_replace( + array('<activity_streams-photo>','</activity_streams-photo>'), + array('<asphoto>','</asphoto>'), + $msg['message']); + + + $parsed_xml = parse_xml_string($msg['message'],false); + + $xmlbase = $parsed_xml->post; + + logger('diaspora_dispatch: ' . print_r($xmlbase,true), LOGGER_DEBUG); + + + if($xmlbase->request) { + $ret = diaspora_request($importer,$xmlbase->request); + } + elseif($xmlbase->status_message) { + $ret = diaspora_post($importer,$xmlbase->status_message,$msg); + } + elseif($xmlbase->profile) { + $ret = diaspora_profile($importer,$xmlbase->profile,$msg); + } + elseif($xmlbase->comment) { + $ret = diaspora_comment($importer,$xmlbase->comment,$msg); + } + elseif($xmlbase->like) { + $ret = diaspora_like($importer,$xmlbase->like,$msg); + } + elseif($xmlbase->asphoto) { + $ret = diaspora_asphoto($importer,$xmlbase->asphoto,$msg); + } + elseif($xmlbase->reshare) { + $ret = diaspora_reshare($importer,$xmlbase->reshare,$msg); + } + elseif($xmlbase->retraction) { + $ret = diaspora_retraction($importer,$xmlbase->retraction,$msg); + } + elseif($xmlbase->signed_retraction) { + $ret = diaspora_signed_retraction($importer,$xmlbase->signed_retraction,$msg); + } + elseif($xmlbase->relayable_retraction) { + $ret = diaspora_signed_retraction($importer,$xmlbase->relayable_retraction,$msg); + } + elseif($xmlbase->photo) { + $ret = diaspora_photo($importer,$xmlbase->photo,$msg,$attempt); + } + elseif($xmlbase->conversation) { + $ret = diaspora_conversation($importer,$xmlbase->conversation,$msg); + } + elseif($xmlbase->message) { + $ret = diaspora_message($importer,$xmlbase->message,$msg); + } + else { + logger('diaspora_dispatch: unknown message type: ' . print_r($xmlbase,true)); + } + return $ret; +} + +function diaspora_handle_from_contact($contact_id) { + $handle = false; + + logger("diaspora_handle_from_contact: contact id is " . $contact_id, LOGGER_DEBUG); + + $r = q("SELECT * from abook left join xchan on abook_xchan = xchan_hash where abook_id = %d", + intval($contact_id) + ); + if($r) { + $contact = $r[0]; + } + $handle = $contact['xchan_addr']; + return $handle; +} + +function diaspora_get_contact_by_handle($uid,$handle) { + $r = q("SELECT * FROM abook left join xchan on xchan_hash = abook_xchan where xchan_addr = '%s' and abook_channel = %d limit 1", + dbesc($handle), + intval($uid) + ); + if($r) + return $r[0]; + return false; +} + +function find_diaspora_person_by_handle($handle) { + + $person = false; + $update = false; + $got_lock = false; + + $endlessloop = 0; + $maxloops = 10; + + do { + $r = q("select * from xchan where xchan_addr = '%s' limit 1", + dbesc($handle) + ); + if($r) { + $person = $r[0]; + logger('find_diaspora_person_by handle: in cache ' . print_r($r,true), LOGGER_DEBUG); + + // update record occasionally so it doesn't get stale + $d = strtotime($person['updated'] . ' +00:00'); + if($d < strtotime('now - 14 days')) + $update = true; + } + + + // FETCHING PERSON INFORMATION FROM REMOTE SERVER + // + // If the person isn't in our 'fcontact' table, or if he/she is but + // his/her information hasn't been updated for more than 14 days, then + // we want to fetch the person's information from the remote server. + // + // Note that $person isn't changed by this block of code unless the + // person's information has been successfully fetched from the remote + // server. So if $person was 'false' to begin with (because he/she wasn't + // in the local cache), it'll stay false, and if $person held the local + // cache information to begin with, it'll keep that information. That way + // if there's a problem with the remote fetch, we can at least use our + // cached information--it's better than nothing. + +//fixme!!! + + if((! $person) || ($update)) { + // Lock the function to prevent race conditions if multiple items + // come in at the same time from a person who doesn't exist in + // fcontact + // + // Don't loop forever. On the last loop, try to create the contact + // whether the function is locked or not. Maybe the locking thread + // has died or something. At any rate, a duplicate in 'fcontact' + // is a much smaller problem than a deadlocked thread +// $got_lock = lock_function('find_diaspora_person_by_handle', false); + if(($endlessloop + 1) >= $maxloops) + $got_lock = true; + + if($got_lock) { + logger('find_diaspora_person_by_handle: create or refresh', LOGGER_DEBUG); + require_once('include/Scrape.php'); + $r = probe_url($handle, PROBE_DIASPORA); + + // Note that Friendica contacts can return a "Diaspora person" + // if Diaspora connectivity is enabled on their server + if((count($r)) && ($r['network'] === NETWORK_DIASPORA)) { + add_fcontact($r,$update); + $person = ($r); + } + +// unlock_function('find_diaspora_person_by_handle'); + } + else { + logger('find_diaspora_person_by_handle: couldn\'t lock function', LOGGER_DEBUG); +// if(! $person) +// block_on_function_lock('find_diaspora_person_by_handle'); + } + } + } while((! $person) && (! $got_lock) && (++$endlessloop < $maxloops)); + + // We need to try again if the person wasn't in 'fcontact' but the function was locked. + // The fact that the function was locked may mean that another process was creating the + // person's record. It could also mean another process was creating or updating an unrelated + // person. + // + // At any rate, we need to keep trying until we've either got the person or had a chance to + // try to fetch his/her remote information. But we don't want to block on locking the + // function, because if the other process is creating the record, then when we acquire the lock + // we'll dive right into creating another, duplicate record. We DO want to at least wait + // until the lock is released, so we don't flood the database with requests. + // + // If the person was in the 'fcontact' table, don't try again. It's not worth the time, since + // we do have some information for the person + + return $person; +} + + +function get_diaspora_key($uri) { + logger('Fetching diaspora key for: ' . $uri); + + $r = find_diaspora_person_by_handle($uri); + if($r) + return $r['pubkey']; + return ''; +} + + +function diaspora_pubmsg_build($msg,$user,$contact,$prvkey,$pubkey) { + + $a = get_app(); + + logger('diaspora_pubmsg_build: ' . $msg, LOGGER_DATA); + + + $handle = $user['xchan_addr']; + + $b64url_data = base64url_encode($msg); + + $data = str_replace(array("\n","\r"," ","\t"),array('','','',''),$b64url_data); + + $type = 'application/xml'; + $encoding = 'base64url'; + $alg = 'RSA-SHA256'; + + $signable_data = $data . '.' . base64url_encode($type) . '.' + . base64url_encode($encoding) . '.' . base64url_encode($alg) ; + + $signature = rsa_sign($signable_data,$prvkey); + $sig = base64url_encode($signature); + +$magic_env = <<< EOT +<?xml version='1.0' encoding='UTF-8'?> +<diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env" > + <header> + <author_id>$handle</author_id> + </header> + <me:env> + <me:encoding>base64url</me:encoding> + <me:alg>RSA-SHA256</me:alg> + <me:data type="application/xml">$data</me:data> + <me:sig>$sig</me:sig> + </me:env> +</diaspora> +EOT; + + logger('diaspora_pubmsg_build: magic_env: ' . $magic_env, LOGGER_DATA); + return $magic_env; + +} + + + + +function diaspora_msg_build($msg,$user,$contact,$prvkey,$pubkey,$public = false) { + $a = get_app(); + + if($public) + return diaspora_pubmsg_build($msg,$user,$contact,$prvkey,$pubkey); + + logger('diaspora_msg_build: ' . $msg, LOGGER_DATA); + + // without a public key nothing will work + + if(! $pubkey) { + logger('diaspora_msg_build: pubkey missing: contact id: ' . $contact['abook_id']); + return ''; + } + + $inner_aes_key = random_string(32); + $b_inner_aes_key = base64_encode($inner_aes_key); + $inner_iv = random_string(16); + $b_inner_iv = base64_encode($inner_iv); + + $outer_aes_key = random_string(32); + $b_outer_aes_key = base64_encode($outer_aes_key); + $outer_iv = random_string(16); + $b_outer_iv = base64_encode($outer_iv); + + $handle = $user['xchan_addr']; + + $padded_data = pkcs5_pad($msg,16); + $inner_encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $inner_aes_key, $padded_data, MCRYPT_MODE_CBC, $inner_iv); + + $b64_data = base64_encode($inner_encrypted); + + + $b64url_data = base64url_encode($b64_data); + $data = str_replace(array("\n","\r"," ","\t"),array('','','',''),$b64url_data); + + $type = 'application/xml'; + $encoding = 'base64url'; + $alg = 'RSA-SHA256'; + + $signable_data = $data . '.' . base64url_encode($type) . '.' + . base64url_encode($encoding) . '.' . base64url_encode($alg) ; + + $signature = rsa_sign($signable_data,$prvkey); + $sig = base64url_encode($signature); + +$decrypted_header = <<< EOT +<decrypted_header> + <iv>$b_inner_iv</iv> + <aes_key>$b_inner_aes_key</aes_key> + <author_id>$handle</author_id> +</decrypted_header> +EOT; + + $decrypted_header = pkcs5_pad($decrypted_header,16); + + $ciphertext = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $outer_aes_key, $decrypted_header, MCRYPT_MODE_CBC, $outer_iv); + + $outer_json = json_encode(array('iv' => $b_outer_iv,'key' => $b_outer_aes_key)); + + $encrypted_outer_key_bundle = ''; + openssl_public_encrypt($outer_json,$encrypted_outer_key_bundle,$pubkey); + + $b64_encrypted_outer_key_bundle = base64_encode($encrypted_outer_key_bundle); + + logger('outer_bundle: ' . $b64_encrypted_outer_key_bundle . ' key: ' . $pubkey, LOGGER_DATA); + + $encrypted_header_json_object = json_encode(array('aes_key' => base64_encode($encrypted_outer_key_bundle), + 'ciphertext' => base64_encode($ciphertext))); + $cipher_json = base64_encode($encrypted_header_json_object); + + $encrypted_header = '<encrypted_header>' . $cipher_json . '</encrypted_header>'; + +$magic_env = <<< EOT +<?xml version='1.0' encoding='UTF-8'?> +<diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env" > + $encrypted_header + <me:env> + <me:encoding>base64url</me:encoding> + <me:alg>RSA-SHA256</me:alg> + <me:data type="application/xml">$data</me:data> + <me:sig>$sig</me:sig> + </me:env> +</diaspora> +EOT; + + logger('diaspora_msg_build: magic_env: ' . $magic_env, LOGGER_DATA); + return $magic_env; + +} + +/** + * + * diaspora_decode($importer,$xml) + * array $importer -> from user table + * string $xml -> urldecoded Diaspora salmon + * + * Returns array + * 'message' -> decoded Diaspora XML message + * 'author' -> author diaspora handle + * 'key' -> author public key (converted to pkcs#8) + * + * Author and key are used elsewhere to save a lookup for verifying replies and likes + */ + + +function diaspora_decode($importer,$xml) { + + $public = false; + $basedom = parse_xml_string($xml); + + $children = $basedom->children('https://joindiaspora.com/protocol'); + + if($children->header) { + $public = true; + $author_link = str_replace('acct:','',$children->header->author_id); + } + else { + + $encrypted_header = json_decode(base64_decode($children->encrypted_header)); + + $encrypted_aes_key_bundle = base64_decode($encrypted_header->aes_key); + $ciphertext = base64_decode($encrypted_header->ciphertext); + + $outer_key_bundle = ''; + openssl_private_decrypt($encrypted_aes_key_bundle,$outer_key_bundle,$importer['prvkey']); + + $j_outer_key_bundle = json_decode($outer_key_bundle); + + $outer_iv = base64_decode($j_outer_key_bundle->iv); + $outer_key = base64_decode($j_outer_key_bundle->key); + + $decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $outer_key, $ciphertext, MCRYPT_MODE_CBC, $outer_iv); + + + $decrypted = pkcs5_unpad($decrypted); + + /** + * $decrypted now contains something like + * + * <decrypted_header> + * <iv>8e+G2+ET8l5BPuW0sVTnQw==</iv> + * <aes_key>UvSMb4puPeB14STkcDWq+4QE302Edu15oaprAQSkLKU=</aes_key> + +***** OBSOLETE + + * <author> + * <name>Ryan Hughes</name> + * <uri>acct:galaxor@diaspora.pirateship.org</uri> + * </author> + +***** CURRENT + + * <author_id>galaxor@diaspora.priateship.org</author_id> + +***** END DIFFS + + * </decrypted_header> + */ + + logger('decrypted: ' . $decrypted, LOGGER_DEBUG); + $idom = parse_xml_string($decrypted,false); + + $inner_iv = base64_decode($idom->iv); + $inner_aes_key = base64_decode($idom->aes_key); + + $author_link = str_replace('acct:','',$idom->author_id); + + } + + $dom = $basedom->children(NAMESPACE_SALMON_ME); + + // figure out where in the DOM tree our data is hiding + + if($dom->provenance->data) + $base = $dom->provenance; + elseif($dom->env->data) + $base = $dom->env; + elseif($dom->data) + $base = $dom; + + if(! $base) { + logger('mod-diaspora: unable to locate salmon data in xml '); + http_status_exit(400); + } + + + // Stash the signature away for now. We have to find their key or it won't be good for anything. + $signature = base64url_decode($base->sig); + + // unpack the data + + // strip whitespace so our data element will return to one big base64 blob + $data = str_replace(array(" ","\t","\r","\n"),array("","","",""),$base->data); + + + // stash away some other stuff for later + + $type = $base->data[0]->attributes()->type[0]; + $keyhash = $base->sig[0]->attributes()->keyhash[0]; + $encoding = $base->encoding; + $alg = $base->alg; + + + $signed_data = $data . '.' . base64url_encode($type) . '.' . base64url_encode($encoding) . '.' . base64url_encode($alg); + + + // decode the data + $data = base64url_decode($data); + + + if($public) { + $inner_decrypted = $data; + } + else { + + // Decode the encrypted blob + + $inner_encrypted = base64_decode($data); + $inner_decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $inner_aes_key, $inner_encrypted, MCRYPT_MODE_CBC, $inner_iv); + $inner_decrypted = pkcs5_unpad($inner_decrypted); + } + + if(! $author_link) { + logger('mod-diaspora: Could not retrieve author URI.'); + http_status_exit(400); + } + + // Once we have the author URI, go to the web and try to find their public key + // (first this will look it up locally if it is in the fcontact cache) + // This will also convert diaspora public key from pkcs#1 to pkcs#8 + + logger('mod-diaspora: Fetching key for ' . $author_link ); + $key = get_diaspora_key($author_link); + + if(! $key) { + logger('mod-diaspora: Could not retrieve author key.'); + http_status_exit(400); + } + + $verify = rsa_verify($signed_data,$signature,$key); + + if(! $verify) { + logger('mod-diaspora: Message did not verify. Discarding.'); + http_status_exit(400); + } + + logger('mod-diaspora: Message verified.'); + + return array('message' => $inner_decrypted, 'author' => $author_link, 'key' => $key); + +} + + +/* sender is now sharing with recipient */ + +function diaspora_request($importer,$xml) { + + $a = get_app(); + + $sender_handle = unxmlify($xml->sender_handle); + $recipient_handle = unxmlify($xml->recipient_handle); + + if(! $sender_handle || ! $recipient_handle) + return; + + + // Do we already have an abook record? + + $contact = diaspora_get_contact_by_handle($importer['channel_id'],$sender_handle); + + if($contact && $contact['abook_id']) { + + // perhaps we were already sharing with this person. Now they're sharing with us. + // That makes us friends. Maybe. + + $newperms = PERMS_R_STREAM|PERMS_R_PROFILE|PERMS_R_PHOTO|PERMS_R_ABOOK|PERMS_W_STREAM|PERMS_W_COMMENT|PERMS_W_MAIL|PERMS_W_CHAT; + + $r = q("update abook set abook_their_perms = %d where abook_id = %d and abook_channel = %d limit 1", + intval($newperms), + intval($contact['abook_id']), + intval($importer['channel_id']) + ); + + return; + } + + $ret = find_diaspora_person_by_handle($sender_handle); + + if((! $ret) || ($ret['xchan_network'] != 'diaspora')) { + logger('diaspora_request: Cannot resolve diaspora handle ' . $sender_handle . ' for ' . $recipient_handle); + return; + } + + $default_perms = 0; + // look for default permissions to apply in return - e.g. auto-friend + $z = q("select * from abook where abook_channel = %d and (abook_flags & %d) limit 1", + intval($importer['channel_id']), + intval(ABOOK_FLAG_SELF) + ); + + if($z) + $default_perms = intval($z[0]['abook_my_perms']); + + $their_perms = PERMS_R_STREAM|PERMS_R_PROFILE|PERMS_R_PHOTO|PERMS_R_ABOOK|PERMS_W_STREAM|PERMS_W_COMMENT|PERMS_W_MAIL|PERMS_CHAT; + + $r = q("insert into abook ( abook_account, abook_channel, abook_xchan, abook_my_perms, abook_their_perms, abook_closeness, abook_rating, abook_created, abook_updated, abook_connected, abook_dob, abook_flags, abook_profile) values ( %d, %d, '%s' %d %d, %d, %d, '%s', '%s', '%s', '%s', %d, '%s')", + intval($importer['channel_account_id']), + intval($importer['channel_id']), + dbesc($ret['xchan_hash']), + intval($default_perms), + intval($their_perms), + intval(99), + intval(0), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + dbesc('0000-00-00 00:00:00'), + intval(($default_perms) ? 0 : ABOOK_FLAG_PENDING) + ); + + + if($r) { + logger("New Diaspora introduction received for {$importer['channel_name']}"); + + $new_connection = q("select * from abook left join xchan on abook_xchan = xchan_hash where abook_channel = %d and abook_xchan = '%s' order by abook_created desc limit 1", + intval($importer['channel_id']), + dbesc($ret['xchan_hash']) + ); + if($new_connection) { + require_once('include/enotify.php'); + notification(array( + 'type' => NOTIFY_INTRO, + 'from_xchan' => $ret['xchan_hash'], + 'to_xchan' => $importer['channel_hash'], + 'link' => z_root() . '/connedit/' . $new_connection[0]['abook_id'], + )); + + if($default_perms) { + // Send back a sharing notification to them + diaspora_share($importer['channel_id'],$new_connection[0]); + + } + } + } + + // find the abook record we just created + + $contact_record = diaspora_get_contact_by_handle($importer['channel_id'],$sender_handle); + + if(! $contact_record) { + logger('diaspora_request: unable to locate newly created contact record.'); + return; + } + +//FIXME +// $g = q("select def_gid from user where uid = %d limit 1", +// intval($importer['channel_id']) +// ); +// if($g && intval($g[0]['def_gid'])) { +// require_once('include/group.php'); +// group_add_member($importer['channel_id'],'',$contact_record['id'],$g[0]['def_gid']); +// } + + + return; +} + + + + +function diaspora_post($importer,$xml,$msg) { + + $a = get_app(); + $guid = notags(unxmlify($xml->guid)); + $diaspora_handle = notags(unxmlify($xml->diaspora_handle)); + + if($diaspora_handle != $msg['author']) { + logger('diaspora_post: Potential forgery. Message handle is not the same as envelope sender.'); + return 202; + } + + $contact = diaspora_get_contact_by_handle($importer['channel_id'],$diaspora_handle); + if(! $contact) + return; + + if(! perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'send_stream')) { + logger('diaspora_post: Ignoring this author.'); + return 202; + } + + $message_id = $diaspora_handle . ':' . $guid; + $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s' AND `guid` = '%s' LIMIT 1", + intval($importer['channel_id']), + dbesc($message_id), + dbesc($guid) + ); + if(count($r)) { + logger('diaspora_post: message exists: ' . $guid); + return; + } + + // allocate a guid on our system - we aren't fixing any collisions. + // we're ignoring them + + $g = q("select * from guid where guid = '%s' limit 1", + dbesc($guid) + ); + if(! count($g)) { + q("insert into guid ( guid ) values ( '%s' )", + dbesc($guid) + ); + } + + $created = unxmlify($xml->created_at); + $private = ((unxmlify($xml->public) == 'false') ? 1 : 0); + + $body = diaspora2bb($xml->raw_message); + + // Add OEmbed and other information to the body + $body = add_page_info_to_body($body, false, true); + + $datarray = array(); + + $str_tags = ''; + + $tags = get_tags($body); + + if(count($tags)) { + foreach($tags as $tag) { + if(strpos($tag,'#') === 0) { + if(strpos($tag,'[url=')) + continue; + + // don't link tags that are already embedded in links + + if(preg_match('/\[(.*?)' . preg_quote($tag,'/') . '(.*?)\]/',$body)) + continue; + if(preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag,'/') . '(.*?)\)/',$body)) + continue; + + $basetag = str_replace('_',' ',substr($tag,1)); + $body = str_replace($tag,'#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]',$body); + if(strlen($str_tags)) + $str_tags .= ','; + $str_tags .= '#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]'; + continue; + } + } + } + + $cnt = preg_match_all('/@\[url=(.*?)\[\/url\]/ism',$body,$matches,PREG_SET_ORDER); + if($cnt) { + foreach($matches as $mtch) { + if(strlen($str_tags)) + $str_tags .= ','; + $str_tags .= '@[url=' . $mtch[1] . '[/url]'; + } + } + + $plink = 'https://'.substr($diaspora_handle,strpos($diaspora_handle,'@')+1).'/posts/'.$guid; + + $datarray['uid'] = $importer['channel_id']; + $datarray['contact-id'] = $contact['id']; + $datarray['wall'] = 0; + $datarray['network'] = NETWORK_DIASPORA; + $datarray['verb'] = ACTIVITY_POST; + $datarray['guid'] = $guid; + $datarray['uri'] = $datarray['parent-uri'] = $message_id; + $datarray['changed'] = $datarray['created'] = $datarray['edited'] = datetime_convert('UTC','UTC',$created); + $datarray['private'] = $private; + $datarray['parent'] = 0; + $datarray['plink'] = $plink; + $datarray['owner-name'] = $contact['name']; + $datarray['owner-link'] = $contact['url']; + //$datarray['owner-avatar'] = $contact['thumb']; + $datarray['owner-avatar'] = ((x($contact,'thumb')) ? $contact['thumb'] : $contact['photo']); + $datarray['author-name'] = $contact['name']; + $datarray['author-link'] = $contact['url']; + $datarray['author-avatar'] = $contact['thumb']; + $datarray['body'] = $body; + $datarray['tag'] = $str_tags; + $datarray['app'] = 'Diaspora'; + + // if empty content it might be a photo that hasn't arrived yet. If a photo arrives, we'll make it visible. + + $datarray['visible'] = ((strlen($body)) ? 1 : 0); + + $message_id = item_store($datarray); + + //if($message_id) { + // q("update item set plink = '%s' where id = %d", + // dbesc($a->get_baseurl() . '/display/' . $importer['nickname'] . '/' . $message_id), + // intval($message_id) + // ); + //} + + return; + +} + +function diaspora_reshare($importer,$xml,$msg) { + + logger('diaspora_reshare: init: ' . print_r($xml,true)); + + $a = get_app(); + $guid = notags(unxmlify($xml->guid)); + $diaspora_handle = notags(unxmlify($xml->diaspora_handle)); + + + if($diaspora_handle != $msg['author']) { + logger('diaspora_post: Potential forgery. Message handle is not the same as envelope sender.'); + return 202; + } + + $contact = diaspora_get_contact_by_handle($importer['channel_id'],$diaspora_handle); + if(! $contact) + return; + + if(! perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'send_stream')) { + logger('diaspora_reshare: Ignoring this author: ' . $diaspora_handle . ' ' . print_r($xml,true)); + return 202; + } + + $message_id = $diaspora_handle . ':' . $guid; + $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s' AND `guid` = '%s' LIMIT 1", + intval($importer['channel_id']), + dbesc($message_id), + dbesc($guid) + ); + if(count($r)) { + logger('diaspora_reshare: message exists: ' . $guid); + return; + } + + $orig_author = notags(unxmlify($xml->root_diaspora_id)); + $orig_guid = notags(unxmlify($xml->root_guid)); + + $source_url = 'https://' . substr($orig_author,strpos($orig_author,'@')+1) . '/p/' . $orig_guid . '.xml'; + $orig_url = 'https://'.substr($orig_author,strpos($orig_author,'@')+1).'/posts/'.$orig_guid; + $x = fetch_url($source_url); + if(! $x) + $x = fetch_url(str_replace('https://','http://',$source_url)); + if(! $x) { + logger('diaspora_reshare: unable to fetch source url ' . $source_url); + return; + } + logger('diaspora_reshare: source: ' . $x); + + $x = str_replace(array('<activity_streams-photo>','</activity_streams-photo>'),array('<asphoto>','</asphoto>'),$x); + $source_xml = parse_xml_string($x,false); + + 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_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_external_images($body); + } + elseif($source_xml->post->status_message) { + $body = diaspora2bb($source_xml->post->status_message->raw_message); + + // Checking for embedded pictures + if($source_xml->post->status_message->photo->remote_photo_path AND + $source_xml->post->status_message->photo->remote_photo_name) { + + $remote_photo_path = notags(unxmlify($source_xml->post->status_message->photo->remote_photo_path)); + $remote_photo_name = notags(unxmlify($source_xml->post->status_message->photo->remote_photo_name)); + + $body = '[img]'.$remote_photo_path.$remote_photo_name.'[/img]'."\n".$body; + + logger('diaspora_reshare: embedded picture link found: '.$body, LOGGER_DEBUG); + } + + $body = scale_external_images($body); + + // Add OEmbed and other information to the body + $body = add_page_info_to_body($body, false, true); + } + else { + // Maybe it is a reshare of a photo that will be delivered at a later time (testing) + logger('diaspora_reshare: no reshare content found: ' . print_r($source_xml,true)); + $body = ""; + //return; + } + + //if(! $body) { + // logger('diaspora_reshare: empty body: source= ' . $x); + // return; + //} + + $person = find_diaspora_person_by_handle($orig_author); + + /*if(is_array($person) && x($person,'name') && x($person,'url')) + $details = '[url=' . $person['url'] . ']' . $person['name'] . '[/url]'; + else + $details = $orig_author; + + $prefix = html_entity_decode("♲ ", ENT_QUOTES, 'UTF-8') . $details . "\n";*/ + + + // allocate a guid on our system - we aren't fixing any collisions. + // we're ignoring them + + $g = q("select * from guid where guid = '%s' limit 1", + dbesc($guid) + ); + if(! count($g)) { + q("insert into guid ( guid ) values ( '%s' )", + dbesc($guid) + ); + } + + $created = unxmlify($xml->created_at); + $private = ((unxmlify($xml->public) == 'false') ? 1 : 0); + + $datarray = array(); + + $str_tags = ''; + + $tags = get_tags($body); + + if(count($tags)) { + foreach($tags as $tag) { + if(strpos($tag,'#') === 0) { + if(strpos($tag,'[url=')) + continue; + + // don't link tags that are already embedded in links + + if(preg_match('/\[(.*?)' . preg_quote($tag,'/') . '(.*?)\]/',$body)) + continue; + if(preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag,'/') . '(.*?)\)/',$body)) + continue; + + + $basetag = str_replace('_',' ',substr($tag,1)); + $body = str_replace($tag,'#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]',$body); + if(strlen($str_tags)) + $str_tags .= ','; + $str_tags .= '#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]'; + continue; + } + } + } + + $plink = 'https://'.substr($diaspora_handle,strpos($diaspora_handle,'@')+1).'/posts/'.$guid; + + $datarray['uid'] = $importer['channel_id']; + $datarray['contact-id'] = $contact['id']; + $datarray['wall'] = 0; + $datarray['network'] = NETWORK_DIASPORA; + $datarray['guid'] = $guid; + $datarray['uri'] = $datarray['parent-uri'] = $message_id; + $datarray['changed'] = $datarray['created'] = $datarray['edited'] = datetime_convert('UTC','UTC',$created); + $datarray['private'] = $private; + $datarray['parent'] = 0; + $datarray['plink'] = $plink; + $datarray['owner-name'] = $contact['name']; + $datarray['owner-link'] = $contact['url']; + $datarray['owner-avatar'] = ((x($contact,'thumb')) ? $contact['thumb'] : $contact['photo']); + if (!intval(get_config('system','wall-to-wall_share'))) { + $prefix = "[share author='".str_replace(array("'", "[", "]"), array("'", "[", "]"),$person['name']). + "' profile='".$person['url']. + "' avatar='".((x($person,'thumb')) ? $person['thumb'] : $person['photo']). + "' link='".str_replace(array("'", "[", "]"), array("'", "[", "]"),$orig_url)."']"; + $datarray['author-name'] = $contact['name']; + $datarray['author-link'] = $contact['url']; + $datarray['author-avatar'] = $contact['thumb']; + $datarray['body'] = $prefix.$body."[/share]"; + } else { + // Let reshared messages look like wall-to-wall posts + $datarray['author-name'] = $person['name']; + $datarray['author-link'] = $person['url']; + $datarray['author-avatar'] = ((x($person,'thumb')) ? $person['thumb'] : $person['photo']); + $datarray['body'] = $body; + } + + $datarray['tag'] = $str_tags; + $datarray['app'] = 'Diaspora'; + + // if empty content it might be a photo that hasn't arrived yet. If a photo arrives, we'll make it visible. (testing) + $datarray['visible'] = ((strlen($body)) ? 1 : 0); + + $message_id = item_store($datarray); + + //if($message_id) { + // q("update item set plink = '%s' where id = %d", + // dbesc($a->get_baseurl() . '/display/' . $importer['nickname'] . '/' . $message_id), + // intval($message_id) + // ); + //} + + return; + +} + + +function diaspora_asphoto($importer,$xml,$msg) { + logger('diaspora_asphoto called'); + + $a = get_app(); + $guid = notags(unxmlify($xml->guid)); + $diaspora_handle = notags(unxmlify($xml->diaspora_handle)); + + if($diaspora_handle != $msg['author']) { + logger('diaspora_post: Potential forgery. Message handle is not the same as envelope sender.'); + return 202; + } + + $contact = diaspora_get_contact_by_handle($importer['channel_id'],$diaspora_handle); + if(! $contact) + return; + + if(! perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'send_stream')) { + logger('diaspora_asphoto: Ignoring this author.'); + return 202; + } + + $message_id = $diaspora_handle . ':' . $guid; + $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s' AND `guid` = '%s' LIMIT 1", + intval($importer['channel_id']), + dbesc($message_id), + dbesc($guid) + ); + if(count($r)) { + logger('diaspora_asphoto: message exists: ' . $guid); + return; + } + + // allocate a guid on our system - we aren't fixing any collisions. + // we're ignoring them + + $g = q("select * from guid where guid = '%s' limit 1", + dbesc($guid) + ); + if(! count($g)) { + q("insert into guid ( guid ) values ( '%s' )", + dbesc($guid) + ); + } + + $created = unxmlify($xml->created_at); + $private = ((unxmlify($xml->public) == 'false') ? 1 : 0); + + 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_external_images($body,false); + } + elseif($xml->image_url) { + $body = '[img]' . notags(unxmlify($xml->image_url)) . '[/img]' . "\n"; + $body = scale_external_images($body); + } + else { + logger('diaspora_asphoto: no photo url found.'); + return; + } + + $plink = 'https://'.substr($diaspora_handle,strpos($diaspora_handle,'@')+1).'/posts/'.$guid; + + $datarray = array(); + + $datarray['uid'] = $importer['channel_id']; + $datarray['contact-id'] = $contact['id']; + $datarray['wall'] = 0; + $datarray['network'] = NETWORK_DIASPORA; + $datarray['guid'] = $guid; + $datarray['uri'] = $datarray['parent-uri'] = $message_id; + $datarray['changed'] = $datarray['created'] = $datarray['edited'] = datetime_convert('UTC','UTC',$created); + $datarray['private'] = $private; + $datarray['parent'] = 0; + $datarray['plink'] = $plink; + $datarray['owner-name'] = $contact['name']; + $datarray['owner-link'] = $contact['url']; + //$datarray['owner-avatar'] = $contact['thumb']; + $datarray['owner-avatar'] = ((x($contact,'thumb')) ? $contact['thumb'] : $contact['photo']); + $datarray['author-name'] = $contact['name']; + $datarray['author-link'] = $contact['url']; + $datarray['author-avatar'] = $contact['thumb']; + $datarray['body'] = $body; + + $datarray['app'] = 'Diaspora/Cubbi.es'; + + $message_id = item_store($datarray); + + //if($message_id) { + // q("update item set plink = '%s' where id = %d", + // dbesc($a->get_baseurl() . '/display/' . $importer['nickname'] . '/' . $message_id), + // intval($message_id) + // ); + //} + + return; + +} + + + + + + +function diaspora_comment($importer,$xml,$msg) { + + $a = get_app(); + $guid = notags(unxmlify($xml->guid)); + $parent_guid = notags(unxmlify($xml->parent_guid)); + $diaspora_handle = notags(unxmlify($xml->diaspora_handle)); + $target_type = notags(unxmlify($xml->target_type)); + $text = unxmlify($xml->text); + $author_signature = notags(unxmlify($xml->author_signature)); + + $parent_author_signature = (($xml->parent_author_signature) ? notags(unxmlify($xml->parent_author_signature)) : ''); + + $contact = diaspora_get_contact_by_handle($importer['channel_id'],$msg['author']); + if(! $contact) { + logger('diaspora_comment: cannot find contact: ' . $msg['author']); + return; + } + + if(! perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'post_comments')) { + logger('diaspora_comment: Ignoring this author.'); + return 202; + } + + $r = q("SELECT * FROM `item` WHERE `uid` = %d AND `guid` = '%s' LIMIT 1", + intval($importer['channel_id']), + dbesc($guid) + ); + if(count($r)) { + logger('diaspora_comment: our comment just got relayed back to us (or there was a guid collision) : ' . $guid); + return; + } + + $r = q("SELECT * FROM `item` WHERE `uid` = %d AND `guid` = '%s' LIMIT 1", + intval($importer['channel_id']), + dbesc($parent_guid) + ); + if(! count($r)) { + logger('diaspora_comment: parent item not found: parent: ' . $parent_guid . ' item: ' . $guid); + return; + } + $parent_item = $r[0]; + + + /* How Diaspora performs comment signature checking: + + - If an item has been sent by the comment author to the top-level post owner to relay on + to the rest of the contacts on the top-level post, the top-level post owner should check + the author_signature, then create a parent_author_signature before relaying the comment on + - If an item has been relayed on by the top-level post owner, the contacts who receive it + check only the parent_author_signature. Basically, they trust that the top-level post + owner has already verified the authenticity of anything he/she sends out + - In either case, the signature that get checked is the signature created by the person + who sent the salmon + */ + + $signed_data = $guid . ';' . $parent_guid . ';' . $text . ';' . $diaspora_handle; + $key = $msg['key']; + + if($parent_author_signature) { + // If a parent_author_signature exists, then we've received the comment + // relayed from the top-level post owner. There's no need to check the + // author_signature if the parent_author_signature is valid + + $parent_author_signature = base64_decode($parent_author_signature); + + if(! rsa_verify($signed_data,$parent_author_signature,$key,'sha256')) { + logger('diaspora_comment: top-level owner verification failed.'); + return; + } + } + else { + // If there's no parent_author_signature, then we've received the comment + // from the comment creator. In that case, the person is commenting on + // our post, so he/she must be a contact of ours and his/her public key + // should be in $msg['key'] + + $author_signature = base64_decode($author_signature); + + if(! rsa_verify($signed_data,$author_signature,$key,'sha256')) { + logger('diaspora_comment: comment author verification failed.'); + return; + } + } + + // Phew! Everything checks out. Now create an item. + + // Find the original comment author information. + // We need this to make sure we display the comment author + // information (name and avatar) correctly. + if(strcasecmp($diaspora_handle,$msg['author']) == 0) + $person = $contact; + else { + $person = find_diaspora_person_by_handle($diaspora_handle); + + if(! is_array($person)) { + logger('diaspora_comment: unable to find author details'); + return; + } + } + + $body = diaspora2bb($text); + $message_id = $diaspora_handle . ':' . $guid; + + $datarray = array(); + + $str_tags = ''; + + $tags = get_tags($body); + + if(count($tags)) { + foreach($tags as $tag) { + if(strpos($tag,'#') === 0) { + if(strpos($tag,'[url=')) + continue; + + // don't link tags that are already embedded in links + + if(preg_match('/\[(.*?)' . preg_quote($tag,'/') . '(.*?)\]/',$body)) + continue; + if(preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag,'/') . '(.*?)\)/',$body)) + continue; + + + $basetag = str_replace('_',' ',substr($tag,1)); + $body = str_replace($tag,'#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]',$body); + if(strlen($str_tags)) + $str_tags .= ','; + $str_tags .= '#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]'; + continue; + } + } + } + + $datarray['uid'] = $importer['channel_id']; + $datarray['contact-id'] = $contact['id']; + $datarray['type'] = 'remote-comment'; + $datarray['wall'] = $parent_item['wall']; + $datarray['network'] = NETWORK_DIASPORA; + $datarray['verb'] = ACTIVITY_POST; + $datarray['gravity'] = GRAVITY_COMMENT; + $datarray['guid'] = $guid; + $datarray['uri'] = $message_id; + $datarray['parent-uri'] = $parent_item['uri']; + + // No timestamps for comments? OK, we'll the use current time. + $datarray['changed'] = $datarray['created'] = $datarray['edited'] = datetime_convert(); + $datarray['private'] = $parent_item['private']; + + $datarray['owner-name'] = $parent_item['owner-name']; + $datarray['owner-link'] = $parent_item['owner-link']; + $datarray['owner-avatar'] = $parent_item['owner-avatar']; + + $datarray['author-name'] = $person['name']; + $datarray['author-link'] = $person['url']; + $datarray['author-avatar'] = ((x($person,'thumb')) ? $person['thumb'] : $person['photo']); + $datarray['body'] = $body; + $datarray['tag'] = $str_tags; + + // We can't be certain what the original app is if the message is relayed. + if(($parent_item['origin']) && (! $parent_author_signature)) + $datarray['app'] = 'Diaspora'; + + $message_id = item_store($datarray); + + //if($message_id) { + //q("update item set plink = '%s' where id = %d", + // //dbesc($a->get_baseurl() . '/display/' . $importer['nickname'] . '/' . $message_id), + // dbesc($a->get_baseurl().'/display/'.$datarray['guid']), + // intval($message_id) + //); + //} + + if(($parent_item['origin']) && (! $parent_author_signature)) { + q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ", + intval($message_id), + dbesc($signed_data), + dbesc(base64_encode($author_signature)), + dbesc($diaspora_handle) + ); + + // if the message isn't already being relayed, notify others + // the existence of parent_author_signature means the parent_author or owner + // is already relaying. + + proc_run('php','include/notifier.php','comment-import',$message_id); + } + + $myconv = q("SELECT `author-link`, `author-avatar`, `parent` FROM `item` WHERE `parent-uri` = '%s' AND `uid` = %d AND `parent` != 0 AND `deleted` = 0 ", + dbesc($parent_item['uri']), + intval($importer['channel_id']) + ); + + if(count($myconv)) { + $importer_url = $a->get_baseurl() . '/profile/' . $importer['nickname']; + + foreach($myconv as $conv) { + + // now if we find a match, it means we're in this conversation + + if(! link_compare($conv['author-link'],$importer_url)) + continue; + + require_once('include/enotify.php'); + + $conv_parent = $conv['parent']; + + notification(array( + 'type' => NOTIFY_COMMENT, + 'notify_flags' => $importer['notify-flags'], + 'language' => $importer['language'], + 'to_name' => $importer['username'], + 'to_email' => $importer['email'], + 'uid' => $importer['channel_id'], + 'item' => $datarray, + //'link' => $a->get_baseurl() . '/display/' . $importer['nickname'] . '/' . $message_id, + 'link' => $a->get_baseurl().'/display/'.$datarray['guid'], + 'source_name' => $datarray['author-name'], + 'source_link' => $datarray['author-link'], + 'source_photo' => $datarray['author-avatar'], + 'verb' => ACTIVITY_POST, + 'otype' => 'item', + 'parent' => $conv_parent, + 'parent_uri' => $parent_uri + )); + + // only send one notification + break; + } + } + return; +} + + + + +function diaspora_conversation($importer,$xml,$msg) { + + $a = get_app(); + + $guid = notags(unxmlify($xml->guid)); + $subject = notags(unxmlify($xml->subject)); + $diaspora_handle = notags(unxmlify($xml->diaspora_handle)); + $participant_handles = notags(unxmlify($xml->participant_handles)); + $created_at = datetime_convert('UTC','UTC',notags(unxmlify($xml->created_at))); + + $parent_uri = $diaspora_handle . ':' . $guid; + + $messages = $xml->message; + + if(! count($messages)) { + logger('diaspora_conversation: empty conversation'); + return; + } + + $contact = diaspora_get_contact_by_handle($importer['channel_id'],$msg['author']); + if(! $contact) { + logger('diaspora_conversation: cannot find contact: ' . $msg['author']); + return; + } + + if(($contact['rel'] == CONTACT_IS_FOLLOWER) || ($contact['blocked']) || ($contact['readonly'])) { + logger('diaspora_conversation: Ignoring this author.'); + return 202; + } + + $conversation = null; + + $c = q("select * from conv where uid = %d and guid = '%s' limit 1", + intval($importer['channel_id']), + dbesc($guid) + ); + if(count($c)) + $conversation = $c[0]; + else { + $r = q("insert into conv (uid,guid,creator,created,updated,subject,recips) values(%d, '%s', '%s', '%s', '%s', '%s', '%s') ", + intval($importer['channel_id']), + dbesc($guid), + dbesc($diaspora_handle), + dbesc(datetime_convert('UTC','UTC',$created_at)), + dbesc(datetime_convert()), + dbesc($subject), + dbesc($participant_handles) + ); + if($r) + $c = q("select * from conv where uid = %d and guid = '%s' limit 1", + intval($importer['channel_id']), + dbesc($guid) + ); + if(count($c)) + $conversation = $c[0]; + } + if(! $conversation) { + logger('diaspora_conversation: unable to create conversation.'); + return; + } + + foreach($messages as $mesg) { + + $reply = 0; + + $msg_guid = notags(unxmlify($mesg->guid)); + $msg_parent_guid = notags(unxmlify($mesg->parent_guid)); + $msg_parent_author_signature = notags(unxmlify($mesg->parent_author_signature)); + $msg_author_signature = notags(unxmlify($mesg->author_signature)); + $msg_text = unxmlify($mesg->text); + $msg_created_at = datetime_convert('UTC','UTC',notags(unxmlify($mesg->created_at))); + $msg_diaspora_handle = notags(unxmlify($mesg->diaspora_handle)); + $msg_conversation_guid = notags(unxmlify($mesg->conversation_guid)); + if($msg_conversation_guid != $guid) { + logger('diaspora_conversation: message conversation guid does not belong to the current conversation. ' . $xml); + continue; + } + + $body = diaspora2bb($msg_text); + $message_id = $msg_diaspora_handle . ':' . $msg_guid; + + $author_signed_data = $msg_guid . ';' . $msg_parent_guid . ';' . $msg_text . ';' . unxmlify($mesg->created_at) . ';' . $msg_diaspora_handle . ';' . $msg_conversation_guid; + + $author_signature = base64_decode($msg_author_signature); + + if(strcasecmp($msg_diaspora_handle,$msg['author']) == 0) { + $person = $contact; + $key = $msg['key']; + } + else { + $person = find_diaspora_person_by_handle($msg_diaspora_handle); + + if(is_array($person) && x($person,'pubkey')) + $key = $person['pubkey']; + else { + logger('diaspora_conversation: unable to find author details'); + continue; + } + } + + if(! rsa_verify($author_signed_data,$author_signature,$key,'sha256')) { + logger('diaspora_conversation: verification failed.'); + continue; + } + + if($msg_parent_author_signature) { + $owner_signed_data = $msg_guid . ';' . $msg_parent_guid . ';' . $msg_text . ';' . unxmlify($mesg->created_at) . ';' . $msg_diaspora_handle . ';' . $msg_conversation_guid; + + $parent_author_signature = base64_decode($msg_parent_author_signature); + + $key = $msg['key']; + + if(! rsa_verify($owner_signed_data,$parent_author_signature,$key,'sha256')) { + logger('diaspora_conversation: owner verification failed.'); + continue; + } + } + + $r = q("select id from mail where `uri` = '%s' limit 1", + dbesc($message_id) + ); + if(count($r)) { + logger('diaspora_conversation: duplicate message already delivered.', LOGGER_DEBUG); + continue; + } + + q("insert into mail ( `uid`, `guid`, `convid`, `from-name`,`from-photo`,`from-url`,`contact-id`,`title`,`body`,`seen`,`reply`,`uri`,`parent-uri`,`created`) values ( %d, '%s', %d, '%s', '%s', '%s', %d, '%s', '%s', %d, %d, '%s','%s','%s')", + intval($importer['channel_id']), + dbesc($msg_guid), + intval($conversation['id']), + dbesc($person['name']), + dbesc($person['photo']), + dbesc($person['url']), + intval($contact['id']), + dbesc($subject), + dbesc($body), + 0, + 0, + dbesc($message_id), + dbesc($parent_uri), + dbesc($msg_created_at) + ); + + q("update conv set updated = '%s' where id = %d", + dbesc(datetime_convert()), + intval($conversation['id']) + ); + + require_once('include/enotify.php'); + notification(array( + 'type' => NOTIFY_MAIL, + 'notify_flags' => $importer['notify-flags'], + 'language' => $importer['language'], + 'to_name' => $importer['username'], + 'to_email' => $importer['email'], + 'uid' =>$importer['importer_uid'], + 'item' => array('subject' => $subject, 'body' => $body), + 'source_name' => $person['name'], + 'source_link' => $person['url'], + 'source_photo' => $person['thumb'], + 'verb' => ACTIVITY_POST, + 'otype' => 'mail' + )); + } + + return; +} + +function diaspora_message($importer,$xml,$msg) { + + $a = get_app(); + + $msg_guid = notags(unxmlify($xml->guid)); + $msg_parent_guid = notags(unxmlify($xml->parent_guid)); + $msg_parent_author_signature = notags(unxmlify($xml->parent_author_signature)); + $msg_author_signature = notags(unxmlify($xml->author_signature)); + $msg_text = unxmlify($xml->text); + $msg_created_at = datetime_convert('UTC','UTC',notags(unxmlify($xml->created_at))); + $msg_diaspora_handle = notags(unxmlify($xml->diaspora_handle)); + $msg_conversation_guid = notags(unxmlify($xml->conversation_guid)); + + $parent_uri = $diaspora_handle . ':' . $msg_parent_guid; + + $contact = diaspora_get_contact_by_handle($importer['channel_id'],$msg_diaspora_handle); + if(! $contact) { + logger('diaspora_message: cannot find contact: ' . $msg_diaspora_handle); + return; + } + + if(($contact['rel'] == CONTACT_IS_FOLLOWER) || ($contact['blocked']) || ($contact['readonly'])) { + logger('diaspora_message: Ignoring this author.'); + return 202; + } + + $conversation = null; + + $c = q("select * from conv where uid = %d and guid = '%s' limit 1", + intval($importer['channel_id']), + dbesc($msg_conversation_guid) + ); + if(count($c)) + $conversation = $c[0]; + else { + logger('diaspora_message: conversation not available.'); + return; + } + + $reply = 0; + + $body = diaspora2bb($msg_text); + $message_id = $msg_diaspora_handle . ':' . $msg_guid; + + $author_signed_data = $msg_guid . ';' . $msg_parent_guid . ';' . $msg_text . ';' . unxmlify($xml->created_at) . ';' . $msg_diaspora_handle . ';' . $msg_conversation_guid; + + + $author_signature = base64_decode($msg_author_signature); + + $person = find_diaspora_person_by_handle($msg_diaspora_handle); + if(is_array($person) && x($person,'pubkey')) + $key = $person['pubkey']; + else { + logger('diaspora_message: unable to find author details'); + return; + } + + if(! rsa_verify($author_signed_data,$author_signature,$key,'sha256')) { + logger('diaspora_message: verification failed.'); + return; + } + + $r = q("select id from mail where `uri` = '%s' and uid = %d limit 1", + dbesc($message_id), + intval($importer['channel_id']) + ); + if(count($r)) { + logger('diaspora_message: duplicate message already delivered.', LOGGER_DEBUG); + return; + } + + q("insert into mail ( `uid`, `guid`, `convid`, `from-name`,`from-photo`,`from-url`,`contact-id`,`title`,`body`,`seen`,`reply`,`uri`,`parent-uri`,`created`) values ( %d, '%s', %d, '%s', '%s', '%s', %d, '%s', '%s', %d, %d, '%s','%s','%s')", + intval($importer['channel_id']), + dbesc($msg_guid), + intval($conversation['id']), + dbesc($person['name']), + dbesc($person['photo']), + dbesc($person['url']), + intval($contact['id']), + dbesc($conversation['subject']), + dbesc($body), + 0, + 1, + dbesc($message_id), + dbesc($parent_uri), + dbesc($msg_created_at) + ); + + q("update conv set updated = '%s' where id = %d", + dbesc(datetime_convert()), + intval($conversation['id']) + ); + + return; +} + + +function diaspora_photo($importer,$xml,$msg,$attempt=1) { + + $a = get_app(); + + logger('diaspora_photo: init',LOGGER_DEBUG); + + $remote_photo_path = notags(unxmlify($xml->remote_photo_path)); + + $remote_photo_name = notags(unxmlify($xml->remote_photo_name)); + + $status_message_guid = notags(unxmlify($xml->status_message_guid)); + + $guid = notags(unxmlify($xml->guid)); + + $diaspora_handle = notags(unxmlify($xml->diaspora_handle)); + + $public = notags(unxmlify($xml->public)); + + $created_at = notags(unxmlify($xml_created_at)); + + logger('diaspora_photo: status_message_guid: ' . $status_message_guid, LOGGER_DEBUG); + + $contact = diaspora_get_contact_by_handle($importer['channel_id'],$msg['author']); + if(! $contact) { + logger('diaspora_photo: contact record not found: ' . $msg['author'] . ' handle: ' . $diaspora_handle); + return; + } + + if(! perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'send_stream')) { + logger('diaspora_photo: Ignoring this author.'); + return 202; + } + + $r = q("SELECT * FROM `item` WHERE `uid` = %d AND `guid` = '%s' LIMIT 1", + intval($importer['channel_id']), + dbesc($status_message_guid) + ); + if(! count($r)) { + if($attempt <= 3) { + q("INSERT INTO dsprphotoq (uid, msg, attempt) VALUES (%d, '%s', %d)", + intval($importer['channel_id']), + dbesc(serialize($msg)), + intval($attempt + 1) + ); + } + logger('diaspora_photo: attempt = ' . $attempt . '; status message not found: ' . $status_message_guid . ' for photo: ' . $guid); + return; + } + + $parent_item = $r[0]; + + $link_text = '[img]' . $remote_photo_path . $remote_photo_name . '[/img]' . "\n"; + + $link_text = scale_external_images($link_text, true, + array($remote_photo_name, 'scaled_full_' . $remote_photo_name)); + + if(strpos($parent_item['body'],$link_text) === false) { + $r = q("update item set `body` = '%s', `visible` = 1 where `id` = %d and `uid` = %d", + dbesc($link_text . $parent_item['body']), + intval($parent_item['id']), + intval($parent_item['uid']) + ); + } + + return; +} + + + + +function diaspora_like($importer,$xml,$msg) { + + $a = get_app(); + $guid = notags(unxmlify($xml->guid)); + $parent_guid = notags(unxmlify($xml->parent_guid)); + $diaspora_handle = notags(unxmlify($xml->diaspora_handle)); + $target_type = notags(unxmlify($xml->target_type)); + $positive = notags(unxmlify($xml->positive)); + $author_signature = notags(unxmlify($xml->author_signature)); + + $parent_author_signature = (($xml->parent_author_signature) ? notags(unxmlify($xml->parent_author_signature)) : ''); + + // likes on comments not supported here and likes on photos not supported by Diaspora + +// if($target_type !== 'Post') +// return; + + $contact = diaspora_get_contact_by_handle($importer['channel_id'],$msg['author']); + if(! $contact) { + logger('diaspora_like: cannot find contact: ' . $msg['author']); + return; + } + + + if(! perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'post_comments')) { + logger('diaspora_like: Ignoring this author.'); + return 202; + } + + $r = q("SELECT * FROM `item` WHERE `uid` = %d AND `guid` = '%s' LIMIT 1", + intval($importer['channel_id']), + dbesc($parent_guid) + ); + if(! count($r)) { + logger('diaspora_like: parent item not found: ' . $guid); + return; + } + + $parent_item = $r[0]; + + $r = q("SELECT * FROM `item` WHERE `uid` = %d AND `guid` = '%s' LIMIT 1", + intval($importer['channel_id']), + dbesc($guid) + ); + if(count($r)) { + if($positive === 'true') { + logger('diaspora_like: duplicate like: ' . $guid); + return; + } + // Note: I don't think "Like" objects with positive = "false" are ever actually used + // It looks like "RelayableRetractions" are used for "unlike" instead + if($positive === 'false') { + logger('diaspora_like: received a like with positive set to "false"...ignoring'); +/* q("UPDATE `item` SET `deleted` = 1 WHERE `id` = %d AND `uid` = %d", + intval($r[0]['id']), + intval($importer['channel_id']) + );*/ + // FIXME--actually don't unless it turns out that Diaspora does indeed send out "false" likes + // send notification via proc_run() + return; + } + } + // Note: I don't think "Like" objects with positive = "false" are ever actually used + // It looks like "RelayableRetractions" are used for "unlike" instead + if($positive === 'false') { + logger('diaspora_like: received a like with positive set to "false"'); + logger('diaspora_like: unlike received with no corresponding like...ignoring'); + return; + } + + + /* How Diaspora performs "like" signature checking: + + - If an item has been sent by the like author to the top-level post owner to relay on + to the rest of the contacts on the top-level post, the top-level post owner should check + the author_signature, then create a parent_author_signature before relaying the like on + - If an item has been relayed on by the top-level post owner, the contacts who receive it + check only the parent_author_signature. Basically, they trust that the top-level post + owner has already verified the authenticity of anything he/she sends out + - In either case, the signature that get checked is the signature created by the person + who sent the salmon + */ + + $signed_data = $guid . ';' . $target_type . ';' . $parent_guid . ';' . $positive . ';' . $diaspora_handle; + $key = $msg['key']; + + if($parent_author_signature) { + // If a parent_author_signature exists, then we've received the like + // relayed from the top-level post owner. There's no need to check the + // author_signature if the parent_author_signature is valid + + $parent_author_signature = base64_decode($parent_author_signature); + + if(! rsa_verify($signed_data,$parent_author_signature,$key,'sha256')) { + if (intval(get_config('system','ignore_diaspora_like_signature'))) + logger('diaspora_like: top-level owner verification failed. Proceeding anyway.'); + else { + logger('diaspora_like: top-level owner verification failed.'); + return; + } + } + } + else { + // If there's no parent_author_signature, then we've received the like + // from the like creator. In that case, the person is "like"ing + // our post, so he/she must be a contact of ours and his/her public key + // should be in $msg['key'] + + $author_signature = base64_decode($author_signature); + + if(! rsa_verify($signed_data,$author_signature,$key,'sha256')) { + if (intval(get_config('system','ignore_diaspora_like_signature'))) + logger('diaspora_like: like creator verification failed. Proceeding anyway'); + else { + logger('diaspora_like: like creator verification failed.'); + return; + } + } + } + + // Phew! Everything checks out. Now create an item. + + // Find the original comment author information. + // We need this to make sure we display the comment author + // information (name and avatar) correctly. + if(strcasecmp($diaspora_handle,$msg['author']) == 0) + $person = $contact; + else { + $person = find_diaspora_person_by_handle($diaspora_handle); + + if(! is_array($person)) { + logger('diaspora_like: unable to find author details'); + return; + } + } + + $uri = $diaspora_handle . ':' . $guid; + + $activity = ACTIVITY_LIKE; + $post_type = (($parent_item['resource-id']) ? t('photo') : t('status')); + $objtype = (($parent_item['resource-id']) ? ACTIVITY_OBJ_PHOTO : ACTIVITY_OBJ_NOTE ); + $link = xmlify('<link rel="alternate" type="text/html" href="' . $a->get_baseurl() . '/display/' . $importer['nickname'] . '/' . $parent_item['id'] . '" />' . "\n") ; + $body = $parent_item['body']; + + $obj = <<< EOT + + <object> + <type>$objtype</type> + <local>1</local> + <id>{$parent_item['uri']}</id> + <link>$link</link> + <title></title> + <content>$body</content> + </object> +EOT; + $bodyverb = t('%1$s likes %2$s\'s %3$s'); + + $arr = array(); + + $arr['uri'] = $uri; + $arr['uid'] = $importer['channel_id']; + $arr['guid'] = $guid; + $arr['network'] = NETWORK_DIASPORA; + $arr['contact-id'] = $contact['id']; + $arr['type'] = 'activity'; + $arr['wall'] = $parent_item['wall']; + $arr['gravity'] = GRAVITY_LIKE; + $arr['parent'] = $parent_item['id']; + $arr['parent-uri'] = $parent_item['uri']; + + $arr['owner-name'] = $parent_item['name']; + $arr['owner-link'] = $parent_item['url']; + //$arr['owner-avatar'] = $parent_item['thumb']; + $arr['owner-avatar'] = ((x($parent_item,'thumb')) ? $parent_item['thumb'] : $parent_item['photo']); + + $arr['author-name'] = $person['name']; + $arr['author-link'] = $person['url']; + $arr['author-avatar'] = ((x($person,'thumb')) ? $person['thumb'] : $person['photo']); + + $ulink = '[url=' . $contact['url'] . ']' . $contact['name'] . '[/url]'; + $alink = '[url=' . $parent_item['author-link'] . ']' . $parent_item['author-name'] . '[/url]'; + //$plink = '[url=' . $a->get_baseurl() . '/display/' . $importer['nickname'] . '/' . $parent_item['id'] . ']' . $post_type . '[/url]'; + $plink = '[url='.$a->get_baseurl().'/display/'.$guid.']'.$post_type.'[/url]'; + $arr['body'] = sprintf( $bodyverb, $ulink, $alink, $plink ); + + $arr['app'] = 'Diaspora'; + + $arr['private'] = $parent_item['private']; + $arr['verb'] = $activity; + $arr['object-type'] = $objtype; + $arr['object'] = $obj; + $arr['visible'] = 1; + $arr['unseen'] = 1; + $arr['last-child'] = 0; + + $message_id = item_store($arr); + + + //if($message_id) { + // q("update item set plink = '%s' where id = %d", + // //dbesc($a->get_baseurl() . '/display/' . $importer['nickname'] . '/' . $message_id), + // dbesc($a->get_baseurl().'/display/'.$guid), + // intval($message_id) + // ); + //} + + if(! $parent_author_signature) { + q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ", + intval($message_id), + dbesc($signed_data), + dbesc(base64_encode($author_signature)), + dbesc($diaspora_handle) + ); + } + + // if the message isn't already being relayed, notify others + // the existence of parent_author_signature means the parent_author or owner + // is already relaying. The parent_item['origin'] indicates the message was created on our system + + if(($parent_item['origin']) && (! $parent_author_signature)) + proc_run('php','include/notifier.php','comment-import',$message_id); + + return; +} + +function diaspora_retraction($importer,$xml) { + + + $guid = notags(unxmlify($xml->guid)); + $diaspora_handle = notags(unxmlify($xml->diaspora_handle)); + $type = notags(unxmlify($xml->type)); + + $contact = diaspora_get_contact_by_handle($importer['channel_id'],$diaspora_handle); + if(! $contact) + return; + + if($type === 'Person') { + require_once('include/Contact.php'); + contact_remove($contact['id']); + } + elseif($type === 'Post') { + $r = q("select * from item where guid = '%s' and uid = %d and not file like '%%[%%' limit 1", + dbesc('guid'), + intval($importer['channel_id']) + ); + if(count($r)) { + if(link_compare($r[0]['author-link'],$contact['url'])) { + q("update item set `deleted` = 1, `changed` = '%s' where `id` = %d", + dbesc(datetime_convert()), + intval($r[0]['id']) + ); + } + } + } + + return 202; + // NOTREACHED +} + +function diaspora_signed_retraction($importer,$xml,$msg) { + + + $guid = notags(unxmlify($xml->target_guid)); + $diaspora_handle = notags(unxmlify($xml->sender_handle)); + $type = notags(unxmlify($xml->target_type)); + $sig = notags(unxmlify($xml->target_author_signature)); + + $parent_author_signature = (($xml->parent_author_signature) ? notags(unxmlify($xml->parent_author_signature)) : ''); + + $contact = diaspora_get_contact_by_handle($importer['channel_id'],$diaspora_handle); + if(! $contact) { + logger('diaspora_signed_retraction: no contact ' . $diaspora_handle . ' for ' . $importer['channel_id']); + return; + } + + + $signed_data = $guid . ';' . $type ; + $key = $msg['key']; + + /* How Diaspora performs relayable_retraction signature checking: + + - If an item has been sent by the item author to the top-level post owner to relay on + to the rest of the contacts on the top-level post, the top-level post owner checks + the author_signature, then creates a parent_author_signature before relaying the item on + - If an item has been relayed on by the top-level post owner, the contacts who receive it + check only the parent_author_signature. Basically, they trust that the top-level post + owner has already verified the authenticity of anything he/she sends out + - In either case, the signature that get checked is the signature created by the person + who sent the salmon + */ + + if($parent_author_signature) { + + $parent_author_signature = base64_decode($parent_author_signature); + + if(! rsa_verify($signed_data,$parent_author_signature,$key,'sha256')) { + logger('diaspora_signed_retraction: top-level post owner verification failed'); + return; + } + + } + else { + + $sig_decode = base64_decode($sig); + + if(! rsa_verify($signed_data,$sig_decode,$key,'sha256')) { + logger('diaspora_signed_retraction: retraction owner verification failed.' . print_r($msg,true)); + return; + } + } + + if($type === 'StatusMessage' || $type === 'Comment' || $type === 'Like') { + $r = q("select * from item where guid = '%s' and uid = %d and not file like '%%[%%' limit 1", + dbesc($guid), + intval($importer['channel_id']) + ); + if(count($r)) { + if(link_compare($r[0]['author-link'],$contact['url'])) { + q("update item set `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = '' where `id` = %d", + dbesc(datetime_convert()), + dbesc(datetime_convert()), + intval($r[0]['id']) + ); + + // Now check if the retraction needs to be relayed by us + // + // The first item in the `item` table with the parent id is the parent. However, MySQL doesn't always + // return the items ordered by `item`.`id`, in which case the wrong item is chosen as the parent. + // The only item with `parent` and `id` as the parent id is the parent item. + $p = q("select origin from item where parent = %d and id = %d limit 1", + $r[0]['parent'], + $r[0]['parent'] + ); + if(count($p)) { + if(($p[0]['origin']) && (! $parent_author_signature)) { + q("insert into sign (`retract_iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ", + $r[0]['id'], + dbesc($signed_data), + dbesc($sig), + dbesc($diaspora_handle) + ); + + // the existence of parent_author_signature would have meant the parent_author or owner + // is already relaying. + logger('diaspora_signed_retraction: relaying relayable_retraction'); + + proc_run('php','include/notifier.php','drop',$r[0]['id']); + } + } + } + } + } + else + logger('diaspora_signed_retraction: unknown type: ' . $type); + + return 202; + // NOTREACHED +} + +function diaspora_profile($importer,$xml,$msg) { + + $a = get_app(); + $diaspora_handle = notags(unxmlify($xml->diaspora_handle)); + + + if($diaspora_handle != $msg['author']) { + logger('diaspora_post: Potential forgery. Message handle is not the same as envelope sender.'); + return 202; + } + + $contact = diaspora_get_contact_by_handle($importer['channel_id'],$diaspora_handle); + if(! $contact) + return; + + if($contact['blocked']) { + logger('diaspora_post: Ignoring this author.'); + return 202; + } + + $name = unxmlify($xml->first_name) . ((strlen($xml->last_name)) ? ' ' . unxmlify($xml->last_name) : ''); + $image_url = unxmlify($xml->image_url); + $birthday = unxmlify($xml->birthday); + + + $handle_parts = explode("@", $diaspora_handle); + if($name === '') { + $name = $handle_parts[0]; + } + + + if( preg_match("|^https?://|", $image_url) === 0) { + $image_url = "http://" . $handle_parts[1] . $image_url; + } + +/* $r = q("SELECT DISTINCT ( `resource-id` ) FROM `photo` WHERE `uid` = %d AND `contact-id` = %d AND `album` = 'Contact Photos' ", + intval($importer['channel_id']), + intval($contact['id']) + ); + $oldphotos = ((count($r)) ? $r : null);*/ + + require_once('include/Photo.php'); + + $images = import_profile_photo($image_url,$importer['channel_id'],$contact['id']); + + // Generic birthday. We don't know the timezone. The year is irrelevant. + + $birthday = str_replace('1000','1901',$birthday); + + $birthday = datetime_convert('UTC','UTC',$birthday,'Y-m-d'); + + // this is to prevent multiple birthday notifications in a single year + // if we already have a stored birthday and the 'm-d' part hasn't changed, preserve the entry, which will preserve the notify year + + 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", + dbesc($name), + dbesc(datetime_convert()), + dbesc($images[0]), + dbesc($images[1]), + dbesc($images[2]), + dbesc(datetime_convert()), + dbesc($birthday), + intval($contact['id']), + intval($importer['channel_id']) + ); + +/* if($r) { + if($oldphotos) { + foreach($oldphotos as $ph) { + q("DELETE FROM `photo` WHERE `uid` = %d AND `contact-id` = %d AND `album` = 'Contact Photos' AND `resource-id` = '%s' ", + intval($importer['channel_id']), + intval($contact['id']), + dbesc($ph['resource-id']) + ); + } + } + } */ + + return; + +} + +function diaspora_share($me,$contact) { + $a = get_app(); + $myaddr = $me['nickname'] . '@' . substr($a->get_baseurl(), strpos($a->get_baseurl(),'://') + 3); + $theiraddr = $contact['addr']; + + $tpl = get_markup_template('diaspora_share.tpl'); + $msg = replace_macros($tpl, array( + '$sender' => $myaddr, + '$recipient' => $theiraddr + )); + + $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$me,$contact,$me['prvkey'],$contact['pubkey']))); + //$slap = 'xml=' . urlencode(diaspora_msg_build($msg,$me,$contact,$me['prvkey'],$contact['pubkey'])); + + return(diaspora_transmit($owner,$contact,$slap, false)); +} + +function diaspora_unshare($me,$contact) { + + $a = get_app(); + $myaddr = $me['nickname'] . '@' . substr($a->get_baseurl(), strpos($a->get_baseurl(),'://') + 3); + + $tpl = get_markup_template('diaspora_retract.tpl'); + $msg = replace_macros($tpl, array( + '$guid' => $me['guid'], + '$type' => 'Person', + '$handle' => $myaddr + )); + + $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$me,$contact,$me['prvkey'],$contact['pubkey']))); + //$slap = 'xml=' . urlencode(diaspora_msg_build($msg,$me,$contact,$me['prvkey'],$contact['pubkey'])); + + return(diaspora_transmit($owner,$contact,$slap, false)); + +} + + +function diaspora_send_status($item,$owner,$contact,$public_batch = false) { + + $a = get_app(); + $myaddr = $owner['nickname'] . '@' . substr($a->get_baseurl(), strpos($a->get_baseurl(),'://') + 3); + $theiraddr = $contact['addr']; + + $images = array(); + + $title = $item['title']; + $body = $item['body']; + +/* + // We're trying to match Diaspora's split message/photo protocol but + // all the photos are displayed on D* as links and not img's - even + // though we're sending pretty much precisely what they send us when + // doing the same operation. + // Commented out for now, we'll use bb2diaspora to convert photos to markdown + // which seems to get through intact. + + $cnt = preg_match_all('|\[img\](.*?)\[\/img\]|',$body,$matches,PREG_SET_ORDER); + if($cnt) { + foreach($matches as $mtch) { + $detail = array(); + $detail['str'] = $mtch[0]; + $detail['path'] = dirname($mtch[1]) . '/'; + $detail['file'] = basename($mtch[1]); + $detail['guid'] = $item['guid']; + $detail['handle'] = $myaddr; + $images[] = $detail; + $body = str_replace($detail['str'],$mtch[1],$body); + } + } +*/ + + //if(strlen($title)) + // $body = "[b]".html_entity_decode($title)."[/b]\n\n".$body; + + // convert to markdown + $body = xmlify(html_entity_decode(bb2diaspora($body))); + //$body = bb2diaspora($body); + + // Adding the title + if(strlen($title)) + $body = "## ".html_entity_decode($title)."\n\n".$body; + + if($item['attach']) { + $cnt = preg_match_all('/href=\"(.*?)\"(.*?)title=\"(.*?)\"/ism',$item['attach'],$matches,PREG_SET_ORDER); + if(cnt) { + $body .= "\n" . t('Attachments:') . "\n"; + foreach($matches as $mtch) { + $body .= '[' . $mtch[3] . '](' . $mtch[1] . ')' . "\n"; + } + } + } + + + $public = (($item['private']) ? 'false' : 'true'); + + require_once('include/datetime.php'); + $created = datetime_convert('UTC','UTC',$item['created'],'Y-m-d H:i:s \U\T\C'); + + // Detect a share element and do a reshare + // see: https://github.com/Raven24/diaspora-federation/blob/master/lib/diaspora-federation/entities/reshare.rb + if (!$item['private'] AND ($ret = diaspora_is_reshare($item["body"]))) { + $tpl = get_markup_template('diaspora_reshare.tpl'); + $msg = replace_macros($tpl, array( + '$root_handle' => xmlify($ret['root_handle']), + '$root_guid' => $ret['root_guid'], + '$guid' => $item['guid'], + '$handle' => xmlify($myaddr), + '$public' => $public, + '$created' => $created, + '$provider' => $item["app"] + )); + } else { + $tpl = get_markup_template('diaspora_post.tpl'); + $msg = replace_macros($tpl, array( + '$body' => $body, + '$guid' => $item['guid'], + '$handle' => xmlify($myaddr), + '$public' => $public, + '$created' => $created, + '$provider' => $item["app"] + )); + } + + logger('diaspora_send_status: '.$owner['username'].' -> '.$contact['name'].' base message: '.$msg, LOGGER_DATA); + + $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['uprvkey'],$contact['pubkey'],$public_batch))); + //$slap = 'xml=' . urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['uprvkey'],$contact['pubkey'],$public_batch)); + + $return_code = diaspora_transmit($owner,$contact,$slap,$public_batch); + + logger('diaspora_send_status: guid: '.$item['guid'].' result '.$return_code, LOGGER_DEBUG); + + if(count($images)) { + diaspora_send_images($item,$owner,$contact,$images,$public_batch); + } + + return $return_code; +} + +function diaspora_is_reshare($body) { + $body = trim($body); + + // Skip if it isn't a pure repeated messages + // Does it start with a share? + if (strpos($body, "[share") > 0) + return(false); + + // Does it end with a share? + if (strlen($body) > (strrpos($body, "[/share]") + 8)) + return(false); + + $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$1",$body); + // Skip if there is no shared message in there + if ($body == $attributes) + return(false); + + $profile = ""; + preg_match("/profile='(.*?)'/ism", $attributes, $matches); + if ($matches[1] != "") + $profile = $matches[1]; + + preg_match('/profile="(.*?)"/ism', $attributes, $matches); + if ($matches[1] != "") + $profile = $matches[1]; + + $ret= array(); + + $ret["root_handle"] = preg_replace("=https?://(.*)/u/(.*)=ism", "$2@$1", $profile); + if (($ret["root_handle"] == $profile) OR ($ret["root_handle"] == "")) + return(false); + + $link = ""; + preg_match("/link='(.*?)'/ism", $attributes, $matches); + if ($matches[1] != "") + $link = $matches[1]; + + preg_match('/link="(.*?)"/ism', $attributes, $matches); + if ($matches[1] != "") + $link = $matches[1]; + + $ret["root_guid"] = preg_replace("=https?://(.*)/posts/(.*)=ism", "$2", $link); + if (($ret["root_guid"] == $link) OR ($ret["root_guid"] == "")) + return(false); + + return($ret); +} + +function diaspora_send_images($item,$owner,$contact,$images,$public_batch = false) { + $a = get_app(); + if(! count($images)) + return; + $mysite = substr($a->get_baseurl(),strpos($a->get_baseurl(),'://') + 3) . '/photo'; + + $tpl = get_markup_template('diaspora_photo.tpl'); + foreach($images as $image) { + if(! stristr($image['path'],$mysite)) + continue; + $resource = str_replace('.jpg','',$image['file']); + $resource = substr($resource,0,strpos($resource,'-')); + + $r = q("select * from photo where `resource-id` = '%s' and `uid` = %d limit 1", + dbesc($resource), + intval($owner['uid']) + ); + if(! count($r)) + continue; + $public = (($r[0]['allow_cid'] || $r[0]['allow_gid'] || $r[0]['deny_cid'] || $r[0]['deny_gid']) ? 'false' : 'true' ); + $msg = replace_macros($tpl,array( + '$path' => xmlify($image['path']), + '$filename' => xmlify($image['file']), + '$msg_guid' => xmlify($image['guid']), + '$guid' => xmlify($r[0]['guid']), + '$handle' => xmlify($image['handle']), + '$public' => xmlify($public), + '$created_at' => xmlify(datetime_convert('UTC','UTC',$r[0]['created'],'Y-m-d H:i:s \U\T\C')) + )); + + + logger('diaspora_send_photo: base message: ' . $msg, LOGGER_DATA); + $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['uprvkey'],$contact['pubkey'],$public_batch))); + //$slap = 'xml=' . urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['uprvkey'],$contact['pubkey'],$public_batch)); + + diaspora_transmit($owner,$contact,$slap,$public_batch); + } + +} + +function diaspora_send_followup($item,$owner,$contact,$public_batch = false) { + + $a = get_app(); + $myaddr = $owner['nickname'] . '@' . substr($a->get_baseurl(), strpos($a->get_baseurl(),'://') + 3); +// $theiraddr = $contact['addr']; + + // Diaspora doesn't support threaded comments, but some + // versions of Diaspora (i.e. Diaspora-pistos) support + // likes on comments + if($item['verb'] === ACTIVITY_LIKE && $item['thr-parent']) { + $p = q("select guid, type, uri, `parent-uri` from item where uri = '%s' limit 1", + dbesc($item['thr-parent']) + ); + } + else { + // The first item in the `item` table with the parent id is the parent. However, MySQL doesn't always + // return the items ordered by `item`.`id`, in which case the wrong item is chosen as the parent. + // The only item with `parent` and `id` as the parent id is the parent item. + $p = q("select guid, type, uri, `parent-uri` from item where parent = %d and id = %d limit 1", + intval($item['parent']), + intval($item['parent']) + ); + } + if(count($p)) + $parent = $p[0]; + else + return; + + if($item['verb'] === ACTIVITY_LIKE) { + $tpl = get_markup_template('diaspora_like.tpl'); + $like = true; + $target_type = ( $parent['uri'] === $parent['parent-uri'] ? 'Post' : 'Comment'); +// $target_type = (strpos($parent['type'], 'comment') ? 'Comment' : 'Post'); +// $positive = (($item['deleted']) ? 'false' : 'true'); + $positive = 'true'; + + if(($item['deleted'])) + logger('diaspora_send_followup: received deleted "like". Those should go to diaspora_send_retraction'); + } + else { + $tpl = get_markup_template('diaspora_comment.tpl'); + $like = false; + } + + $text = html_entity_decode(bb2diaspora($item['body'])); + + // sign it + + if($like) + $signed_text = $item['guid'] . ';' . $target_type . ';' . $parent['guid'] . ';' . $positive . ';' . $myaddr; + else + $signed_text = $item['guid'] . ';' . $parent['guid'] . ';' . $text . ';' . $myaddr; + + $authorsig = base64_encode(rsa_sign($signed_text,$owner['uprvkey'],'sha256')); + + $msg = replace_macros($tpl,array( + '$guid' => xmlify($item['guid']), + '$parent_guid' => xmlify($parent['guid']), + '$target_type' =>xmlify($target_type), + '$authorsig' => xmlify($authorsig), + '$body' => xmlify($text), + '$positive' => xmlify($positive), + '$handle' => xmlify($myaddr) + )); + + logger('diaspora_followup: base message: ' . $msg, LOGGER_DATA); + + $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['uprvkey'],$contact['pubkey'],$public_batch))); + //$slap = 'xml=' . urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['uprvkey'],$contact['pubkey'],$public_batch)); + + return(diaspora_transmit($owner,$contact,$slap,$public_batch)); +} + + +function diaspora_send_relay($item,$owner,$contact,$public_batch = false) { + + + $a = get_app(); + $myaddr = $owner['nickname'] . '@' . substr($a->get_baseurl(), strpos($a->get_baseurl(),'://') + 3); +// $theiraddr = $contact['addr']; + + $body = $item['body']; + $text = html_entity_decode(bb2diaspora($body)); + + // Diaspora doesn't support threaded comments, but some + // versions of Diaspora (i.e. Diaspora-pistos) support + // likes on comments + if($item['verb'] === ACTIVITY_LIKE && $item['thr-parent']) { + $p = q("select guid, type, uri, `parent-uri` from item where uri = '%s' limit 1", + dbesc($item['thr-parent']) + ); + } + else { + // The first item in the `item` table with the parent id is the parent. However, MySQL doesn't always + // return the items ordered by `item`.`id`, in which case the wrong item is chosen as the parent. + // The only item with `parent` and `id` as the parent id is the parent item. + $p = q("select guid, type, uri, `parent-uri` from item where parent = %d and id = %d limit 1", + intval($item['parent']), + intval($item['parent']) + ); + } + if(count($p)) + $parent = $p[0]; + else + return; + + $like = false; + $relay_retract = false; + $sql_sign_id = 'iid'; + if( $item['deleted']) { + $relay_retract = true; + + $target_type = ( ($item['verb'] === ACTIVITY_LIKE) ? 'Like' : 'Comment'); + + $sql_sign_id = 'retract_iid'; + $tpl = get_markup_template('diaspora_relayable_retraction.tpl'); + } + elseif($item['verb'] === ACTIVITY_LIKE) { + $like = true; + + $target_type = ( $parent['uri'] === $parent['parent-uri'] ? 'Post' : 'Comment'); +// $positive = (($item['deleted']) ? 'false' : 'true'); + $positive = 'true'; + + $tpl = get_markup_template('diaspora_like_relay.tpl'); + } + else { // item is a comment + $tpl = get_markup_template('diaspora_comment_relay.tpl'); + } + + + // fetch the original signature if the relayable was created by a Diaspora + // or DFRN user. Relayables for other networks are not supported. + +/* $r = q("select * from sign where " . $sql_sign_id . " = %d limit 1", + intval($item['id']) + ); + if(count($r)) { + $orig_sign = $r[0]; + $signed_text = $orig_sign['signed_text']; + $authorsig = $orig_sign['signature']; + $handle = $orig_sign['signer']; + } + else { + + // Author signature information (for likes, comments, and retractions of likes or comments, + // whether from Diaspora or Friendica) must be placed in the `sign` table before this + // function is called + logger('diaspora_send_relay: original author signature not found, cannot send relayable'); + return; + }*/ + + /* Since the author signature is only checked by the parent, not by the relay recipients, + * I think it may not be necessary for us to do so much work to preserve all the original + * signatures. The important thing that Diaspora DOES need is the original creator's handle. + * Let's just generate that and forget about all the original author signature stuff. + * + * Note: this might be more of an problem if we want to support likes on comments for older + * versions of Diaspora (diaspora-pistos), but since there are a number of problems with + * doing that, let's ignore it for now. + * + * Currently, only DFRN contacts are supported. StatusNet shouldn't be hard, but it hasn't + * been done yet + */ + + $handle = diaspora_handle_from_contact($item['contact-id']); + if(! $handle) + return; + + + if($relay_retract) + $sender_signed_text = $item['guid'] . ';' . $target_type; + elseif($like) + $sender_signed_text = $item['guid'] . ';' . $target_type . ';' . $parent['guid'] . ';' . $positive . ';' . $handle; + else + $sender_signed_text = $item['guid'] . ';' . $parent['guid'] . ';' . $text . ';' . $handle; + + // Sign the relayable with the top-level owner's signature + // + // We'll use the $sender_signed_text that we just created, instead of the $signed_text + // stored in the database, because that provides the best chance that Diaspora will + // be able to reconstruct the signed text the same way we did. This is particularly a + // concern for the comment, whose signed text includes the text of the comment. The + // smallest change in the text of the comment, including removing whitespace, will + // make the signature verification fail. Since we translate from BB code to Diaspora's + // markup at the top of this function, which is AFTER we placed the original $signed_text + // in the database, it's hazardous to trust the original $signed_text. + + $parentauthorsig = base64_encode(rsa_sign($sender_signed_text,$owner['uprvkey'],'sha256')); + + $msg = replace_macros($tpl,array( + '$guid' => xmlify($item['guid']), + '$parent_guid' => xmlify($parent['guid']), + '$target_type' =>xmlify($target_type), + '$authorsig' => xmlify($authorsig), + '$parentsig' => xmlify($parentauthorsig), + '$body' => xmlify($text), + '$positive' => xmlify($positive), + '$handle' => xmlify($handle) + )); + + logger('diaspora_send_relay: base message: ' . $msg, LOGGER_DATA); + + + $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['uprvkey'],$contact['pubkey'],$public_batch))); + //$slap = 'xml=' . urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['uprvkey'],$contact['pubkey'],$public_batch)); + + return(diaspora_transmit($owner,$contact,$slap,$public_batch)); + +} + + + +function diaspora_send_retraction($item,$owner,$contact,$public_batch = false) { + + $a = get_app(); + $myaddr = $owner['nickname'] . '@' . substr($a->get_baseurl(), strpos($a->get_baseurl(),'://') + 3); + + // Check whether the retraction is for a top-level post or whether it's a relayable + if( $item['uri'] !== $item['parent-uri'] ) { + + $tpl = get_markup_template('diaspora_relay_retraction.tpl'); + $target_type = (($item['verb'] === ACTIVITY_LIKE) ? 'Like' : 'Comment'); + } + else { + + $tpl = get_markup_template('diaspora_signed_retract.tpl'); + $target_type = 'StatusMessage'; + } + + $signed_text = $item['guid'] . ';' . $target_type; + + $msg = replace_macros($tpl, array( + '$guid' => xmlify($item['guid']), + '$type' => xmlify($target_type), + '$handle' => xmlify($myaddr), + '$signature' => xmlify(base64_encode(rsa_sign($signed_text,$owner['uprvkey'],'sha256'))) + )); + + $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['uprvkey'],$contact['pubkey'],$public_batch))); + //$slap = 'xml=' . urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['uprvkey'],$contact['pubkey'],$public_batch)); + + return(diaspora_transmit($owner,$contact,$slap,$public_batch)); +} + +function diaspora_send_mail($item,$owner,$contact) { + + $a = get_app(); + $myaddr = $owner['nickname'] . '@' . substr($a->get_baseurl(), strpos($a->get_baseurl(),'://') + 3); + + $r = q("select * from conv where id = %d and uid = %d limit 1", + intval($item['convid']), + intval($item['uid']) + ); + + if(! count($r)) { + logger('diaspora_send_mail: conversation not found.'); + return; + } + $cnv = $r[0]; + + $conv = array( + 'guid' => xmlify($cnv['guid']), + 'subject' => xmlify($cnv['subject']), + 'created_at' => xmlify(datetime_convert('UTC','UTC',$cnv['created'],'Y-m-d H:i:s \U\T\C')), + 'diaspora_handle' => xmlify($cnv['creator']), + 'participant_handles' => xmlify($cnv['recips']) + ); + + $body = bb2diaspora($item['body']); + $created = datetime_convert('UTC','UTC',$item['created'],'Y-m-d H:i:s \U\T\C'); + + $signed_text = $item['guid'] . ';' . $cnv['guid'] . ';' . $body . ';' + . $created . ';' . $myaddr . ';' . $cnv['guid']; + + $sig = base64_encode(rsa_sign($signed_text,$owner['uprvkey'],'sha256')); + + $msg = array( + 'guid' => xmlify($item['guid']), + 'parent_guid' => xmlify($cnv['guid']), + 'parent_author_signature' => (($item['reply']) ? null : xmlify($sig)), + 'author_signature' => xmlify($sig), + 'text' => xmlify($body), + 'created_at' => xmlify($created), + 'diaspora_handle' => xmlify($myaddr), + 'conversation_guid' => xmlify($cnv['guid']) + ); + + if($item['reply']) { + $tpl = get_markup_template('diaspora_message.tpl'); + $xmsg = replace_macros($tpl, array('$msg' => $msg)); + } + else { + $conv['messages'] = array($msg); + $tpl = get_markup_template('diaspora_conversation.tpl'); + $xmsg = replace_macros($tpl, array('$conv' => $conv)); + } + + logger('diaspora_conversation: ' . print_r($xmsg,true), LOGGER_DATA); + + $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($xmsg,$owner,$contact,$owner['uprvkey'],$contact['pubkey'],false))); + //$slap = 'xml=' . urlencode(diaspora_msg_build($xmsg,$owner,$contact,$owner['uprvkey'],$contact['pubkey'],false)); + + return(diaspora_transmit($owner,$contact,$slap,false)); + + +} + +function diaspora_transmit($owner,$contact,$slap,$public_batch,$queue_run=false) { + + $enabled = intval(get_config('system','diaspora_enabled')); + if(! $enabled) { + return 200; + } + + $a = get_app(); + $logid = random_string(4); + $dest_url = (($public_batch) ? $contact['batch'] : $contact['notify']); + if(! $dest_url) { + logger('diaspora_transmit: no url for contact: ' . $contact['id'] . ' batch mode =' . $public_batch); + return 0; + } + + logger('diaspora_transmit: ' . $logid . ' ' . $dest_url); + + if( (! $queue_run) && (was_recently_delayed($contact['id'])) ) { + $return_code = 0; + } + else { + if (!intval(get_config('system','diaspora_test'))) { + post_url($dest_url . '/', $slap); + $return_code = $a->get_curl_code(); + } else { + logger('diaspora_transmit: test_mode'); + return 200; + } + } + + logger('diaspora_transmit: ' . $logid . ' returns: ' . $return_code); + + if((! $return_code) || (($return_code == 503) && (stristr($a->get_curl_headers(),'retry-after')))) { + logger('diaspora_transmit: queue message'); + + $r = q("SELECT id from queue where cid = %d and network = '%s' and content = '%s' and batch = %d limit 1", + intval($contact['id']), + dbesc(NETWORK_DIASPORA), + dbesc($slap), + intval($public_batch) + ); + if(count($r)) { + logger('diaspora_transmit: add_to_queue ignored - identical item already in queue'); + } + else { + // queue message for redelivery + add_to_queue($contact['id'],NETWORK_DIASPORA,$slap,$public_batch); + } + } + + + return(($return_code) ? $return_code : (-1)); +} + + diff --git a/include/dir_fns.php b/include/dir_fns.php index 1bc97cfec..815b6a197 100644 --- a/include/dir_fns.php +++ b/include/dir_fns.php @@ -64,26 +64,40 @@ function sync_directories($dirmode) { if($dirmode == DIRECTORY_MODE_STANDALONE || $dirmode == DIRECTORY_MODE_NORMAL) return; - $r = q("select * from site where (site_flags & %d) and site_url != '%s'", - intval(DIRECTORY_MODE_PRIMARY|DIRECTORY_MODE_SECONDARY), - dbesc(z_root()) - ); + $realm = get_directory_realm(); + if($realm == DIRECTORY_REALM) { + $r = q("select * from site where (site_flags & %d) and site_url != '%s' and ( site_realm = '%s' or site_realm = '') ", + intval(DIRECTORY_MODE_PRIMARY|DIRECTORY_MODE_SECONDARY), + dbesc(z_root()), + dbesc($realm) + ); + } + else { + $r = q("select * from site where (site_flags & %d) and site_url != '%s' and site_realm like '%s' ", + intval(DIRECTORY_MODE_PRIMARY|DIRECTORY_MODE_SECONDARY), + dbesc(z_root()), + dbesc(protect_sprintf('%' . $realm . '%')) + ); + } // If there are no directory servers, setup the fallback master + // FIXME - what to do if we're in a different realm? if((! $r) && (z_root() != DIRECTORY_FALLBACK_MASTER)) { $r = array( 'site_url' => DIRECTORY_FALLBACK_MASTER, 'site_flags' => DIRECTORY_MODE_PRIMARY, 'site_update' => '0000-00-00 00:00:00', - 'site_directory' => DIRECTORY_FALLBACK_MASTER . '/dirsearch' + 'site_directory' => DIRECTORY_FALLBACK_MASTER . '/dirsearch', + 'site_realm' => DIRECTORY_REALM ); - $x = q("insert into site ( site_url, site_flags, site_update, site_directory ) - values ( '%s', %d', '%s', '%s' ) ", + $x = q("insert into site ( site_url, site_flags, site_update, site_directory, site_realm ) + values ( '%s', %d', '%s', '%s', '%s' ) ", dbesc($r[0]['site_url']), intval($r[0]['site_flags']), dbesc($r[0]['site_update']), - dbesc($r[0]['site_directory']) + dbesc($r[0]['site_directory']), + dbesc($r[0]['site_realm']) ); $r = q("select * from site where (site_flags & %d) and site_url != '%s'", @@ -185,6 +199,7 @@ function local_dir_update($uid,$force) { ); $profile = array(); + $profile['encoding'] = 'zot'; if($p) { $hash = $p[0]['channel_hash']; diff --git a/include/enotify.php b/include/enotify.php index 8baf5c09f..2503f9ab0 100644 --- a/include/enotify.php +++ b/include/enotify.php @@ -65,7 +65,7 @@ function notification($params) { localize_item($i); $title = $i['title']; $body = $i['body']; - $private = $i['item_private']; + $private = (($i['item_private']) || ($i['item_flags'] & ITEM_OBSCURED)); } else { $title = $params['item']['title']; @@ -135,7 +135,7 @@ function notification($params) { $item_post_type = item_post_type($p[0]); - $private = $p[0]['item_private']; +// $private = $p[0]['item_private']; $parent_id = $p[0]['id']; //$possess_desc = str_replace('<!item_type!>',$possess_desc); @@ -411,7 +411,7 @@ function notification($params) { $textversion = strip_tags(html_entity_decode(bbcode(stripslashes(str_replace(array("\\r", "\\n"), array( "", "\n"), $body))),ENT_QUOTES,'UTF-8')); - $htmlversion = html_entity_decode(bbcode(stripslashes(str_replace(array("\\r","\\n"), array("","<br />\n"),$body))), ENT_QUOTES,'UTF-8'); + $htmlversion = bbcode(stripslashes(str_replace(array("\\r","\\n"), array("","<br />\n"),$body))); // use $_SESSION['zid_override'] to force zid() to use @@ -461,6 +461,8 @@ function notification($params) { // Might be interesting to use GPG,PGP,S/MIME encryption instead // but we'll save that for a clever plugin developer to implement + $private_activity = false; + if(! $datarray['email_secure']) { switch($params['type']) { case NOTIFY_WALL: @@ -469,14 +471,22 @@ function notification($params) { case NOTIFY_COMMENT: if(! $private) break; + $private_activity = true; case NOTIFY_MAIL: $datarray['textversion'] = $datarray['htmlversion'] = $datarray['title'] = ''; + $datarray['subject'] = preg_replace('/' . preg_quote(t('[Red:Notify]')) . '/','$0*',$datarray['subject']); break; default: break; } } + if($private_activity + && intval(get_pconfig($datarray['uid'],'system','ignore_private_notifications'))) { + pop_lang(); + return; + } + // load the template for private message notifications $tpl = get_markup_template('email_notify_html.tpl'); $email_html_body = replace_macros($tpl,array( diff --git a/include/externals.php b/include/externals.php index 1d9fd2902..a96bf7c97 100644 --- a/include/externals.php +++ b/include/externals.php @@ -33,8 +33,32 @@ function externals_run($argv, $argc){ $url = $r[0]['site_url']; } + // Note: blacklisted sites must be stored in the config as an array. + // No simple way to turn this into a personal config because we have no identity here. + // For that we probably need a variant of superblock. + + $blacklisted = false; + $bl1 = get_config('system','blacklisted_sites'); + if(is_array($bl1) && $bl1) { + foreach($bl1 as $bl) { + if(strpos($url,$bl) !== false) { + $blacklisted = true; + break; + } + } + } + $attempts ++; + // make sure we can eventually break out if somebody blacklists all known sites + + if($blacklisted) { + if($attempts > 20) + break; + $attempts --; + continue; + } + if($url) { if($r[0]['site_pull'] !== '0000-00-00 00:00:00') $mindate = urlencode($r[0]['site_pull']); diff --git a/include/features.php b/include/features.php index a2698cc72..6bb444cb6 100644 --- a/include/features.php +++ b/include/features.php @@ -24,6 +24,8 @@ function get_features() { // This is per post, and different from fixed expiration 'expire' which isn't working yet array('content_expire', t('Content Expiration'), t('Remove posts/comments and/or private messages at a future time')), array('multi_profiles', t('Multiple Profiles'), t('Ability to create multiple profiles')), + array('advanced_profiles', t('Advanced Profiles'), t('Additional profile sections and selections')), + array('profile_export', t('Profile Import/Export'), t('Save and load profile details across sites/channels')), array('webpages', t('Web Pages'), t('Provide managed web pages on your channel')), array('private_notes', t('Private Notes'), t('Enables a tool to store notes and reminders')), // prettyphoto has licensing issues and will no longer be provided in core - diff --git a/include/identity.php b/include/identity.php index e210b37ab..9335673a0 100644 --- a/include/identity.php +++ b/include/identity.php @@ -559,6 +559,37 @@ function profile_load(&$a, $nickname, $profile = '') { return; } + $q = q("select * from profext where hash = '%s' and channel_id = %d", + dbesc($p[0]['profile_guid']), + intval($p[0]['profile_uid']) + ); + if($q) { + + $extra_fields = array(); + + require_once('include/identity.php'); + $profile_fields_basic = get_profile_fields_basic(); + $profile_fields_advanced = get_profile_fields_advanced(); + + $advanced = ((feature_enabled(local_user(),'advanced_profiles')) ? true : false); + if($advanced) + $fields = $profile_fields_advanced; + else + $fields = $profile_fields_basic; + + foreach($q as $qq) { + foreach($fields as $k => $f) { + if($k == $qq['k']) { + $p[0][$k] = $qq['v']; + $extra_fields[] = $k; + break; + } + } + } + } + + $p[0]['extra_fields'] = $extra_fields; + $z = q("select xchan_photo_date from xchan where xchan_hash = '%s' limit 1", dbesc($p[0]['channel_hash']) ); @@ -952,7 +983,7 @@ function advanced_profile(&$a) { if($a->profile['gender']) $profile['gender'] = array( t('Gender:'), $a->profile['gender'] ); $ob_hash = get_observer_hash(); - if($ob_hash && perm_is_allowed($a->profile['profile_uid'],$ob_hash,'post_wall')) { + if($ob_hash && perm_is_allowed($a->profile['profile_uid'],$ob_hash,'post_like')) { $profile['canlike'] = true; $profile['likethis'] = t('Like this channel'); $profile['profile_guid'] = $a->profile['profile_guid']; @@ -1041,6 +1072,16 @@ function advanced_profile(&$a) { if($txt = prepare_text($a->profile['education'])) $profile['education'] = array( t('School/education:'), $txt ); + if($a->profile['extra_fields']) { + foreach($a->profile['extra_fields'] as $f) { + $x = q("select * from profdef where field_name = '%s' limit 1", + dbesc($f) + ); + if($x && $txt = prepare_text($a->profile[$f])) + $profile[$f] = array( $x[0]['field_desc'] . ':',$txt); + } + $profile['extra_fields'] = $a->profile['extra_fields']; + } $things = get_things($a->profile['profile_guid'],$a->profile['profile_uid']); @@ -1312,3 +1353,38 @@ function is_public_profile() { return true; return false; } + +function get_profile_fields_basic($filter = 0) { + + $profile_fields_basic = (($filter == 0) ? get_config('system','profile_fields_basic') : null); + if(! $profile_fields_basic) + $profile_fields_basic = array('name','pdesc','chandesc','gender','dob','dob_tz','address','locality','region','postal_code','country_name','marital','sexual','homepage','hometown','keywords','about','contact'); + + $x = array(); + if($profile_fields_basic) + foreach($profile_fields_basic as $f) + $x[$f] = 1; + + return $x; + +} + + +function get_profile_fields_advanced($filter = 0) { + $basic = get_profile_fields_basic($filter); + $profile_fields_advanced = (($filter == 0) ? get_config('system','profile_fields_advanced') : null); + if(! $profile_fields_advanced) + $profile_fields_advanced = array('with','howlong','politic','religion','likes','dislikes','interest','channels','music','book','film','tv','romance','work','education'); + + $x = array(); + if($basic) + foreach($basic as $f => $v) + $x[$f] = $v; + if($profile_fields_advanced) + foreach($profile_fields_advanced as $f) + $x[$f] = 1; + + return $x; +} + + diff --git a/include/items.php b/include/items.php index 10daa85f2..05ff1b078 100755 --- a/include/items.php +++ b/include/items.php @@ -7,17 +7,13 @@ require_once('include/photo/photo_driver.php'); require_once('include/permissions.php'); -function collect_recipients($item,&$private) { +function collect_recipients($item,&$private_envelope) { require_once('include/group.php'); - $private = ((intval($item['item_private'])) ? true : false); + $private_envelope = ((intval($item['item_private'])) ? true : false); $recipients = array(); - // if the post is marked private but there are no recipients, only add the author and owner - // as recipients. The ACL for the post may live on the hub of a different clone. We need to - // get the post to that hub. - if($item['allow_cid'] || $item['allow_gid'] || $item['deny_cid'] || $item['deny_gid']) { // it is private @@ -49,10 +45,21 @@ function collect_recipients($item,&$private) { $deny = array_unique(array_merge($deny_people,$deny_groups)); $recipients = array_diff($recipients,$deny); - $private = true; + $private_envelope = true; } else { - if(! $private) { + + // if the post is marked private but there are no recipients and public_policy/scope = self, + // only add the author and owner as recipients. The ACL for the post may live on the hub of + // a different clone. We need to get the post to that hub. + + // The post may be private by virtue of not being visible to anybody on the internet, + // but there are no envelope recipients, so set this to false. Delivery is controlled + // by the directives in $item['public_policy']. + + $private_envelope = false; + + if(array_key_exists('public_policy',$item) && $item['public_policy'] !== 'self') { $r = q("select abook_xchan from abook where abook_channel = %d and not (abook_flags & %d) ", intval($item['uid']), intval(ABOOK_FLAG_SELF|ABOOK_FLAG_PENDING|ABOOK_FLAG_ARCHIVED) @@ -664,10 +671,16 @@ function title_is_body($title, $body) { function get_item_elements($x) { - $arr = array(); $arr['body'] = (($x['body']) ? htmlspecialchars($x['body'],ENT_COMPAT,'UTF-8',false) : ''); + $maxlen = get_max_import_size(); + + if($maxlen && mb_strlen($arr['body']) > $maxlen) { + $arr['body'] = mb_substr($arr['body'],0,$maxlen,'UTF-8'); + logger('get_item_elements: message length exceeds max_import_size: truncated'); + } + $arr['created'] = datetime_convert('UTC','UTC',$x['created']); $arr['edited'] = datetime_convert('UTC','UTC',$x['edited']); @@ -703,6 +716,11 @@ function get_item_elements($x) { $arr['mimetype'] = (($x['mimetype']) ? htmlspecialchars($x['mimetype'], ENT_COMPAT,'UTF-8',false) : ''); $arr['obj_type'] = (($x['object_type']) ? htmlspecialchars($x['object_type'], ENT_COMPAT,'UTF-8',false) : ''); $arr['tgt_type'] = (($x['target_type']) ? htmlspecialchars($x['target_type'], ENT_COMPAT,'UTF-8',false) : ''); + + $arr['public_policy'] = (($x['public_scope']) ? htmlspecialchars($x['public_scope'], ENT_COMPAT,'UTF-8',false) : ''); + if($arr['public_policy'] === 'public') + $arr['public_policy'] = ''; + $arr['comment_policy'] = (($x['comment_scope']) ? htmlspecialchars($x['comment_scope'], ENT_COMPAT,'UTF-8',false) : 'contacts'); $arr['sig'] = (($x['signature']) ? htmlspecialchars($x['signature'], ENT_COMPAT,'UTF-8',false) : ''); @@ -838,6 +856,7 @@ function import_author_rss($x) { function encode_item($item) { $x = array(); $x['type'] = 'activity'; + $x['encoding'] = 'zot'; // logger('encode_item: ' . print_r($item,true)); @@ -845,16 +864,15 @@ function encode_item($item) { intval($item['uid']) ); - if($r) { - $public_scope = $r[0]['channel_r_stream']; + if($r) $comment_scope = $r[0]['channel_w_comment']; - } - else { - $public_scope = 0; + else $comment_scope = 0; - } - $scope = map_scope($public_scope); + $scope = $item['public_policy']; + if(! $scope) + $scope = 'public'; + $c_scope = map_scope($comment_scope); if(array_key_exists('item_flags',$item) && ($item['item_flags'] & ITEM_OBSCURED)) { @@ -897,8 +915,7 @@ function encode_item($item) { if($y = encode_item_flags($item)) $x['flags'] = $y; - if(! in_array('private',$y)) - $x['public_scope'] = $scope; + $x['public_scope'] = $scope; if($item['item_flags'] & ITEM_NOCOMMENT) $x['comment_scope'] = 'none'; @@ -915,14 +932,18 @@ function encode_item($item) { } -function map_scope($scope) { +function map_scope($scope,$strip = false) { switch($scope) { case 0: return 'self'; case PERMS_PUBLIC: + if($strip) + return ''; return 'public'; case PERMS_NETWORK: return 'network: red'; + case PERMS_AUTHED: + return 'authenticated'; case PERMS_SITE: return 'site: ' . get_app()->get_hostname(); case PERMS_PENDING: @@ -933,7 +954,22 @@ function map_scope($scope) { } } - +function translate_scope($scope) { + if(! $scope || $scope === 'public') + return t('Visible to anybody on the internet.'); + if(strpos($scope,'self') === 0) + return t('Visible to you only.'); + if(strpos($scope,'network:') === 0) + return t('Visible to anybody in this network.'); + if(strpos($scope,'authenticated') === 0) + return t('Visible to anybody authenticated.'); + if(strpos($scope,'site:') === 0) + return sprintf( t('Visible to anybody on %s.'), strip_tags(substr($scope,6))); + if(strpos($scope,'any connections') === 0) + return t('Visible to all connections.'); + if(strpos($scope,'contacts') === 0) + return t('Visible to approved connections.'); +} function encode_item_xchan($xchan) { @@ -1069,6 +1105,7 @@ function encode_item_flags($item) { function encode_mail($item) { $x = array(); $x['type'] = 'mail'; + $x['encoding'] = 'zot'; if(array_key_exists('mail_flags',$item) && ($item['mail_flags'] & MAIL_OBSCURED)) { $key = get_config('system','prvkey'); @@ -1730,6 +1767,8 @@ function item_store($arr,$allow_exec = false) { $arr['app'] = ((x($arr,'app')) ? notags(trim($arr['app'])) : ''); $arr['item_restrict'] = ((x($arr,'item_restrict')) ? intval($arr['item_restrict']) : 0 ); + $arr['public_policy'] = ((x($arr,'public_policy')) ? notags(trim($arr['public_policy'])) : '' ); + $arr['comment_policy'] = ((x($arr,'comment_policy')) ? notags(trim($arr['comment_policy'])) : 'contacts' ); @@ -1762,6 +1801,7 @@ function item_store($arr,$allow_exec = false) { $allow_gid = $arr['allow_gid']; $deny_cid = $arr['deny_cid']; $deny_gid = $arr['deny_gid']; + $public_policy = $arr['public_policy']; $arr['item_flags'] = $arr['item_flags'] | ITEM_THREAD_TOP; } else { @@ -1798,6 +1838,7 @@ function item_store($arr,$allow_exec = false) { $allow_gid = $r[0]['allow_gid']; $deny_cid = $r[0]['deny_cid']; $deny_gid = $r[0]['deny_gid']; + $public_policy = $r[0]['public_policy']; if($r[0]['item_flags'] & ITEM_WALL) $arr['item_flags'] = $arr['item_flags'] | ITEM_WALL; @@ -1811,10 +1852,7 @@ function item_store($arr,$allow_exec = false) { $uplinked_comment = true; } - // if the parent is private, force privacy for the entire conversation - // This differs from the above settings as it subtly allows comments from - // email correspondents to be private even if the overall thread is not. if($r[0]['item_private']) $arr['item_private'] = $r[0]['item_private']; @@ -1906,7 +1944,7 @@ function item_store($arr,$allow_exec = false) { if((! $parent_id) || ($arr['parent_mid'] === $arr['mid'])) $parent_id = $current_post; - if(strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid)) + if(strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid) || strlen($public_policy)) $private = 1; else $private = $arr['item_private']; @@ -1914,12 +1952,13 @@ function item_store($arr,$allow_exec = false) { // Set parent id - and also make sure to inherit the parent's ACL's. $r = q("UPDATE item SET parent = %d, allow_cid = '%s', allow_gid = '%s', - deny_cid = '%s', deny_gid = '%s', item_private = %d WHERE id = %d LIMIT 1", + deny_cid = '%s', deny_gid = '%s', public_policy = '%s', item_private = %d WHERE id = %d LIMIT 1", intval($parent_id), dbesc($allow_cid), dbesc($allow_gid), dbesc($deny_cid), dbesc($deny_gid), + dbesc($public_policy), intval($private), intval($current_post) ); @@ -1931,6 +1970,7 @@ function item_store($arr,$allow_exec = false) { $arr['allow_gid'] = $allow_gid; $arr['deny_cid'] = $deny_cid; $arr['deny_gid'] = $deny_gid; + $arr['public_policy'] = $public_policy; $arr['item_private'] = $private; // Store taxonomy @@ -2132,6 +2172,11 @@ function item_store_update($arr,$allow_exec = false) { $arr['sig'] = ((x($arr,'sig')) ? $arr['sig'] : ''); $arr['layout_mid'] = ((array_key_exists('layout_mid',$arr)) ? dbesc($arr['layout_mid']) : $orig[0]['layout_mid'] ); + $arr['public_policy'] = ((x($arr,'public_policy')) ? notags(trim($arr['public_policy'])) : $orig[0]['public_policy'] ); + $arr['comment_policy'] = ((x($arr,'comment_policy')) ? notags(trim($arr['comment_policy'])) : $orig[0]['comment_policy'] ); + + + call_hooks('post_remote_update',$arr); if(x($arr,'cancel')) { @@ -2417,6 +2462,8 @@ function tag_deliver($uid,$item_id) { $private = (($u[0]['channel_allow_cid'] || $u[0]['channel_allow_gid'] || $u[0]['channel_deny_cid'] || $u[0]['channel_deny_gid']) ? 1 : 0); +//FIXME - add check for public_policy + $flag_bits = ITEM_WALL|ITEM_ORIGIN; // maintain the original source, which will be the original item owner and was stored in source_xchan @@ -2557,6 +2604,8 @@ function tag_deliver($uid,$item_id) { $private = (($u[0]['channel_allow_cid'] || $u[0]['channel_allow_gid'] || $u[0]['channel_deny_cid'] || $u[0]['channel_deny_gid']) ? 1 : 0); +// FIXME set public_policy and recheck private + $flag_bits = ITEM_WALL|ITEM_ORIGIN|ITEM_UPLINK; // preserve the source diff --git a/include/nav.php b/include/nav.php index 80e4955e5..8133ecf67 100644 --- a/include/nav.php +++ b/include/nav.php @@ -34,6 +34,10 @@ EOT; if(local_user()) { $channel = $a->get_channel(); $observer = $a->get_observer(); + $prof = q("select id from profile where uid = %d and is_default = 1", + intval($channel['channel_id']) + ); + } elseif(remote_user()) $observer = $a->get_observer(); @@ -81,6 +85,9 @@ EOT; $nav['usermenu'][] = Array('profile/' . $channel['channel_address'], t('View Profile'), "", t('Your profile page')); if(feature_enabled(local_user(),'multi_profiles')) $nav['usermenu'][] = Array('profiles', t('Edit Profiles'),"", t('Manage/Edit profiles')); + else + $nav['usermenu'][] = Array('profiles/' . $prof[0]['id'], t('Edit Profile'),"", t('Edit your profile')); + $nav['usermenu'][] = Array('photos/' . $channel['channel_address'], t('Photos'), "", t('Your photos')); $nav['usermenu'][] = Array('cloud/' . $channel['channel_address'],t('Files'),"",t('Your files')); diff --git a/include/permissions.php b/include/permissions.php index facba037f..8e4676f51 100644 --- a/include/permissions.php +++ b/include/permissions.php @@ -10,12 +10,12 @@ function get_perms() { $global_perms = array( // Read only permissions - 'view_stream' => array('channel_r_stream', intval(PERMS_R_STREAM), true, t('Can view my "public" stream and posts'), ''), - 'view_profile' => array('channel_r_profile', intval(PERMS_R_PROFILE), true, t('Can view my "public" channel profile'), ''), - 'view_photos' => array('channel_r_photos', intval(PERMS_R_PHOTOS), true, t('Can view my "public" photo albums'), ''), - 'view_contacts' => array('channel_r_abook', intval(PERMS_R_ABOOK), true, t('Can view my "public" address book'), ''), - 'view_storage' => array('channel_r_storage', intval(PERMS_R_STORAGE), true, t('Can view my "public" file storage'), ''), - 'view_pages' => array('channel_r_pages', intval(PERMS_R_PAGES), true, t('Can view my "public" pages'), ''), + 'view_stream' => array('channel_r_stream', intval(PERMS_R_STREAM), true, t('Can view my normal stream and posts'), ''), + 'view_profile' => array('channel_r_profile', intval(PERMS_R_PROFILE), true, t('Can view my default channel profile'), ''), + 'view_photos' => array('channel_r_photos', intval(PERMS_R_PHOTOS), true, t('Can view my photo albums'), ''), + 'view_contacts' => array('channel_r_abook', intval(PERMS_R_ABOOK), true, t('Can view my connections'), ''), + 'view_storage' => array('channel_r_storage', intval(PERMS_R_STORAGE), true, t('Can view my file storage'), ''), + 'view_pages' => array('channel_r_pages', intval(PERMS_R_PAGES), true, t('Can view my webpages'), ''), // Write permissions 'send_stream' => array('channel_w_stream', intval(PERMS_W_STREAM), false, t('Can send me their channel stream and posts'), ''), @@ -23,14 +23,14 @@ function get_perms() { 'post_comments' => array('channel_w_comment', intval(PERMS_W_COMMENT), false, t('Can comment on or like my posts'), ''), 'post_mail' => array('channel_w_mail', intval(PERMS_W_MAIL), false, t('Can send me private mail messages'), ''), 'post_photos' => array('channel_w_photos', intval(PERMS_W_PHOTOS), false, t('Can post photos to my photo albums'), ''), - 'post_like' => array('channel_w_like', intval(PERMS_W_LIKE), false, t('Can like/dislike stuff'), 'Profiles and things other than posts/comments'), + 'post_like' => array('channel_w_like', intval(PERMS_W_LIKE), false, t('Can like/dislike stuff'), t('Profiles and things other than posts/comments')), 'tag_deliver' => array('channel_w_tagwall', intval(PERMS_W_TAGWALL), false, t('Can forward to all my channel contacts via post @mentions'), t('Advanced - useful for creating group forum channels')), 'chat' => array('channel_w_chat', intval(PERMS_W_CHAT), false, t('Can chat with me (when available)'), t('')), - 'write_storage' => array('channel_w_storage', intval(PERMS_W_STORAGE), false, t('Can write to my "public" file storage'), ''), - 'write_pages' => array('channel_w_pages', intval(PERMS_W_PAGES), false, t('Can edit my "public" pages'), ''), + 'write_storage' => array('channel_w_storage', intval(PERMS_W_STORAGE), false, t('Can write to my file storage'), ''), + 'write_pages' => array('channel_w_pages', intval(PERMS_W_PAGES), false, t('Can edit my webpages'), ''), - 'republish' => array('channel_a_republish', intval(PERMS_A_REPUBLISH), false, t('Can source my "public" posts in derived channels'), t('Somewhat advanced - very useful in open communities')), + 'republish' => array('channel_a_republish', intval(PERMS_A_REPUBLISH), false, t('Can source my public posts in derived channels'), t('Somewhat advanced - very useful in open communities')), 'delegate' => array('channel_a_delegate', intval(PERMS_A_DELEGATE), false, t('Can administer my channel resources'), t('Extremely advanced. Leave this alone unless you know what you are doing')), ); @@ -411,3 +411,63 @@ function site_default_perms() { } return $ret; } + + +/** + * @function get_role_perms($role) + * @param string $role + * + * Given a string for the channel role ('social','forum', etc) + * return an array of all permission fields pre-filled for this role. + * This includes the channel permission scope indicators as well as + * perms_auto: The permissions to apply automatically on receipt of a connection request + * perms_follow: The permissions to apply when initiating a connection request to another channel + * perms_accept: The permissions to apply when accepting a connection request from another channel (not automatic) + * + * Any attributes may be extended (new roles defined) and modified (specific permissions altered) by plugins + * + */ + +function get_role_perms($role) { + + $ret = array(); + + $ret['role'] = $role; + + switch($role) { + case 'social': + $ret['perms_auto'] = 0; + $ret['perms_follow'] = PERMS_R_STREAM|PERMS_R_PROFILE|PERMS_R_PHOTOS|PERMS_R_ABOOK + |PERMS_W_STREAM|PERMS_W_WALL|PERMS_W_COMMENT|PERMS_W_MAIL|PERMS_W_CHAT + |PERMS_R_STORAGE|PERMS_R_PAGES|PERMS_A_REPUBLISH|PERMS_W_LIKE; + $ret['perms_accept'] = PERMS_R_STREAM|PERMS_R_PROFILE|PERMS_R_PHOTOS|PERMS_R_ABOOK + |PERMS_W_STREAM|PERMS_W_WALL|PERMS_W_COMMENT|PERMS_W_MAIL|PERMS_W_CHAT + |PERMS_R_STORAGE|PERMS_R_PAGES|PERMS_A_REPUBLISH|PERMS_W_LIKE; + $ret['channel_r_stream'] = PERMS_PUBLIC; + $ret['channel_r_profile'] = PERMS_PUBLIC; + $ret['channel_r_photos'] = PERMS_PUBLIC; + $ret['channel_r_abook'] = PERMS_PUBLIC; + $ret['channel_w_stream'] = PERMS_CONTACTS; + $ret['channel_w_wall'] = PERMS_CONTACTS; + $ret['channel_w_tagwall'] = PERMS_SPECIFIC; + $ret['channel_w_comment'] = PERMS_CONTACTS; + $ret['channel_w_mail'] = PERMS_CONTACTS; + $ret['channel_w_photos'] = 0; + $ret['channel_w_chat'] = PERMS_CONTACTS; + $ret['channel_a_delegate'] = 0; + $ret['channel_r_storage'] = PERMS_PUBLIC; + $ret['channel_r_pages'] = PERMS_PUBLIC; + $ret['channel_w_pages'] = 0; + $ret['channel_a_republish'] = PERMS_SPECIFIC; + $ret['channel_w_like'] = PERMS_NETWORK; + + break; + + } + + call_hooks('get_role_perms',$ret); + + return $ret; +} + + diff --git a/include/photo/photo_driver.php b/include/photo/photo_driver.php index f5e915402..daf1bfc25 100644 --- a/include/photo/photo_driver.php +++ b/include/photo/photo_driver.php @@ -287,6 +287,10 @@ abstract class photo_driver { $p['allow_gid'] = (($arr['allow_gid']) ? $arr['allow_gid'] : ''); $p['deny_cid'] = (($arr['deny_cid']) ? $arr['deny_cid'] : ''); $p['deny_gid'] = (($arr['deny_gid']) ? $arr['deny_gid'] : ''); + $p['created'] = (($arr['created']) ? $arr['created'] : datetime_convert()); + $p['edited'] = (($arr['edited']) ? $arr['edited'] : $p['created']); + $p['title'] = (($arr['title']) ? $arr['title'] : ''); + $p['description'] = (($arr['description']) ? $arr['description'] : ''); // temporary until we get rid of photo['profile'] and just use photo['photo_flags'] // but this will require updating all existing photos in the DB. @@ -318,6 +322,8 @@ abstract class photo_driver { `scale` = %d, `profile` = %d, `photo_flags` = %d, + `title` = '%s', + `description` = '%s', `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', @@ -328,8 +334,8 @@ abstract class photo_driver { intval($p['uid']), dbesc($p['xchan']), dbesc($p['resource_id']), - dbesc(datetime_convert()), - dbesc(datetime_convert()), + dbesc($p['created']), + dbesc($p['edited']), dbesc(basename($p['filename'])), dbesc($this->getType()), dbesc($p['album']), @@ -340,6 +346,8 @@ abstract class photo_driver { intval($p['scale']), intval($p['profile']), intval($p['photo_flags']), + dbesc($p['title']), + dbesc($p['description']), dbesc($p['allow_cid']), dbesc($p['allow_gid']), dbesc($p['deny_cid']), @@ -349,14 +357,14 @@ abstract class photo_driver { } else { $r = q("INSERT INTO `photo` - ( `aid`, `uid`, `xchan`, `resource_id`, `created`, `edited`, `filename`, type, `album`, `height`, `width`, `data`, `size`, `scale`, `profile`, `photo_flags`, `allow_cid`, `allow_gid`, `deny_cid`, `deny_gid` ) - VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', %d, %d, %d, %d, '%s', '%s', '%s', '%s' )", + ( `aid`, `uid`, `xchan`, `resource_id`, `created`, `edited`, `filename`, type, `album`, `height`, `width`, `data`, `size`, `scale`, `profile`, `photo_flags`, `title`, `description`, `allow_cid`, `allow_gid`, `deny_cid`, `deny_gid` ) + VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', %d, %d, %d, %d, '%s', '%s', '%s', '%s', '%s', '%s' )", intval($p['aid']), intval($p['uid']), dbesc($p['xchan']), dbesc($p['resource_id']), - dbesc(datetime_convert()), - dbesc(datetime_convert()), + dbesc($p['created']), + dbesc($p['edited']), dbesc(basename($p['filename'])), dbesc($this->getType()), dbesc($p['album']), @@ -367,6 +375,8 @@ abstract class photo_driver { intval($p['scale']), intval($p['profile']), intval($p['photo_flags']), + dbesc($p['title']), + dbesc($p['description']), dbesc($p['allow_cid']), dbesc($p['allow_gid']), dbesc($p['deny_cid']), diff --git a/include/photos.php b/include/photos.php index 9819c7ef2..06a99457a 100644 --- a/include/photos.php +++ b/include/photos.php @@ -61,47 +61,64 @@ function photo_upload($channel, $observer, $args) { $str_group_deny = perms2str(((is_array($args['group_deny'])) ? $args['group_deny'] : explode(',',$args['group_deny']))); $str_contact_deny = perms2str(((is_array($args['contact_deny'])) ? $args['contact_deny'] : explode(',',$args['contact_deny']))); - $f = array('src' => '', 'filename' => '', 'filesize' => 0, 'type' => ''); - call_hooks('photo_upload_file',$f); + if($args['data']) { - if(x($f,'src') && x($f,'filesize')) { - $src = $f['src']; - $filename = $f['filename']; - $filesize = $f['filesize']; - $type = $f['type']; + // allow an import from a binary string representing the image. + // This bypasses the upload step and max size limit checking + + $imagedata = $args['data']; + $filename = $args['filename']; + $filesize = strlen($imagedata); + // this is going to be deleted if it exists + $src = '/tmp/deletemenow'; + $type = $args['type']; } else { - $src = $_FILES['userfile']['tmp_name']; - $filename = basename($_FILES['userfile']['name']); - $filesize = intval($_FILES['userfile']['size']); - $type = $_FILES['userfile']['type']; - } + $f = array('src' => '', 'filename' => '', 'filesize' => 0, 'type' => ''); - if (! $type) - $type=guess_image_type($filename); + call_hooks('photo_upload_file',$f); - logger('photo_upload: received file: ' . $filename . ' as ' . $src . ' ('. $type . ') ' . $filesize . ' bytes', LOGGER_DEBUG); + if(x($f,'src') && x($f,'filesize')) { + $src = $f['src']; + $filename = $f['filename']; + $filesize = $f['filesize']; + $type = $f['type']; + } + else { + $src = $_FILES['userfile']['tmp_name']; + $filename = basename($_FILES['userfile']['name']); + $filesize = intval($_FILES['userfile']['size']); + $type = $_FILES['userfile']['type']; + } - $maximagesize = get_config('system','maximagesize'); + if (! $type) + $type=guess_image_type($filename); - if(($maximagesize) && ($filesize > $maximagesize)) { - $ret['message'] = sprintf ( t('Image exceeds website size limit of %lu bytes'), $maximagesize); - @unlink($src); - call_hooks('photo_upload_end',$ret); - return $ret; - } + logger('photo_upload: received file: ' . $filename . ' as ' . $src . ' ('. $type . ') ' . $filesize . ' bytes', LOGGER_DEBUG); - if(! $filesize) { - $ret['message'] = t('Image file is empty.'); - @unlink($src); - call_hooks('photo_post_end',$ret); - return $ret; - } + + $maximagesize = get_config('system','maximagesize'); + + if(($maximagesize) && ($filesize > $maximagesize)) { + $ret['message'] = sprintf ( t('Image exceeds website size limit of %lu bytes'), $maximagesize); + @unlink($src); + call_hooks('photo_upload_end',$ret); + return $ret; + } - logger('photo_upload: loading the contents of ' . $src , LOGGER_DEBUG); + if(! $filesize) { + $ret['message'] = t('Image file is empty.'); + @unlink($src); + call_hooks('photo_post_end',$ret); + return $ret; + } + + logger('photo_upload: loading the contents of ' . $src , LOGGER_DEBUG); + + $imagedata = @file_get_contents($src); + } - $imagedata = @file_get_contents($src); $r = q("select sum(size) as total from photo where aid = %d and scale = 0 ", intval($account_id) @@ -141,7 +158,7 @@ function photo_upload($channel, $observer, $args) { $smallest = 0; - $photo_hash = photo_new_resource(); + $photo_hash = (($args['resource_id']) ? $args['resource_id'] : photo_new_resource()); $visitor = ''; if($channel['channel_hash'] !== $observer['xchan_hash']) @@ -154,6 +171,15 @@ function photo_upload($channel, $observer, $args) { 'allow_cid' => $str_contact_allow, 'allow_gid' => $str_group_allow, 'deny_cid' => $str_contact_deny, 'deny_gid' => $str_group_deny ); + if($args['created']) + $p['created'] = $args['created']; + if($args['edited']) + $p['edited'] = $args['edited']; + if($args['title']) + $p['title'] = $args['title']; + if($args['description']) + $p['desciprion'] = $args['description']; + $r1 = $ph->save($p); if(! $r1) diff --git a/include/profile_selectors.php b/include/profile_selectors.php index 1ffcd49be..a80677cb3 100644 --- a/include/profile_selectors.php +++ b/include/profile_selectors.php @@ -18,6 +18,25 @@ function gender_selector($current="",$suffix="") { return $o; } +function gender_selector_min($current="",$suffix="") { + $o = ''; + $select = array('', t('Male'), t('Female'), t('Other')); + + call_hooks('gender_selector_min', $select); + + $o .= "<select name=\"gender$suffix\" id=\"gender-select$suffix\" size=\"1\" >"; + foreach($select as $selection) { + if($selection !== 'NOTRANSLATION') { + $selected = (($selection == $current) ? ' selected="selected" ' : ''); + $o .= "<option value=\"$selection\" $selected >$selection</option>"; + } + } + $o .= '</select>'; + return $o; +} + + + function sexpref_selector($current="",$suffix="") { $o = ''; $select = array('', t('Males'), t('Females'), t('Gay'), t('Lesbian'), t('No Preference'), t('Bisexual'), t('Autosexual'), t('Abstinent'), t('Virgin'), t('Deviant'), t('Fetish'), t('Oodles'), t('Nonsexual')); @@ -37,6 +56,25 @@ function sexpref_selector($current="",$suffix="") { } +function sexpref_selector_min($current="",$suffix="") { + $o = ''; + $select = array('', t('Males'), t('Females'), t('Other')); + + call_hooks('sexpref_selector_min', $select); + + $o .= "<select name=\"sexual$suffix\" id=\"sexual-select$suffix\" size=\"1\" >"; + foreach($select as $selection) { + if($selection !== 'NOTRANSLATION') { + $selected = (($selection == $current) ? ' selected="selected" ' : ''); + $o .= "<option value=\"$selection\" $selected >$selection</option>"; + } + } + $o .= '</select>'; + return $o; +} + + + function marital_selector($current="",$suffix="") { $o = ''; $select = array('', t('Single'), t('Lonely'), t('Available'), t('Unavailable'), t('Has crush'), t('Infatuated'), t('Dating'), t('Unfaithful'), t('Sex Addict'), t('Friends'), t('Friends/Benefits'), t('Casual'), t('Engaged'), t('Married'), t('Imaginarily married'), t('Partners'), t('Cohabiting'), t('Common law'), t('Happy'), t('Not looking'), t('Swinger'), t('Betrayed'), t('Separated'), t('Unstable'), t('Divorced'), t('Imaginarily divorced'), t('Widowed'), t('Uncertain'), t('It\'s complicated'), t('Don\'t care'), t('Ask me') ); @@ -53,3 +91,20 @@ function marital_selector($current="",$suffix="") { $o .= '</select>'; return $o; } + +function marital_selector_min($current="",$suffix="") { + $o = ''; + $select = array('', t('Single'), t('Dating'), t('Cohabiting'), t('Married'), t('Separated'), t('Divorced'), t('Widowed'), t('It\'s complicated'), t('Other')); + + call_hooks('marital_selector_min', $select); + + $o .= "<select name=\"marital\" id=\"marital-select\" size=\"1\" >"; + foreach($select as $selection) { + if($selection !== 'NOTRANSLATION') { + $selected = (($selection == $current) ? ' selected="selected" ' : ''); + $o .= "<option value=\"$selection\" $selected >$selection</option>"; + } + } + $o .= '</select>'; + return $o; +} diff --git a/include/widgets.php b/include/widgets.php index 96bced87f..1aa018fb6 100644 --- a/include/widgets.php +++ b/include/widgets.php @@ -48,6 +48,13 @@ function widget_collections($args) { $abook_id = 0; $wmode = 0; break; + case 'connections': + $every = 'connections'; + $each = 'group'; + $edit = true; + $current = $_REQUEST['gid']; + $abook_id = 0; + $wmode = 0; case 'groups': $every = 'connections'; $each = argv(0); diff --git a/include/zot.php b/include/zot.php index 0fbeed519..8b0efe09d 100644 --- a/include/zot.php +++ b/include/zot.php @@ -625,6 +625,10 @@ function import_xchan($arr,$ud_flags = UPDATE_FLAGS_UPDATED, $ud_arr = null) { return $ret; } + if(! ($arr['guid'] && $arr['guid_sig'])) { + logger('import_xchan: no identity information provided. ' . print_r($arr,true)); + return $ret; + } $xchan_hash = make_xchan_hash($arr['guid'],$arr['guid_sig']); $import_photos = false; @@ -985,8 +989,22 @@ function import_xchan($arr,$ud_flags = UPDATE_FLAGS_UPDATED, $ud_arr = null) { // Are we a directory server of some kind? + $other_realm = false; + $realm = get_directory_realm(); + if(array_key_exists('site',$arr) + && array_key_exists('realm',$arr['site']) + && (strpos($arr['site']['realm'],$realm) !== false)) + $other_realm = true; + if($dirmode != DIRECTORY_MODE_NORMAL) { - if(array_key_exists('profile',$arr) && is_array($arr['profile'])) { + + // We're some kind of directory server. However we can only add directory information + // if the entry is in the same realm (or is a sub-realm). Sub-realms are denoted by + // including the parent realm in the name. e.g. 'RED_GLOBAL:foo' would allow an entry to + // be in directories for the local realm (foo) and also the RED_GLOBAL realm. + + + if(array_key_exists('profile',$arr) && is_array($arr['profile']) && (! $other_realm)) { $profile_changed = import_directory_profile($xchan_hash,$arr['profile'],$address,$ud_flags, 1); if($profile_changed) { $what .= 'profile '; @@ -1200,6 +1218,12 @@ function zot_import($arr, $sender_url) { continue; } + // It's a specifically targetted post. If we were sent a public_scope hint (likely), + // get rid of it so that it doesn't get stored and cause trouble. + + if(array_key_exists('message',$i) && array_key_exists('public_scope',$i['message'])) + unset($i['message']['public_scope']); + $deliveries = $r; // We found somebody on this site that's in the recipient list. @@ -1207,18 +1231,32 @@ function zot_import($arr, $sender_url) { } else { if(($i['message']) && (array_key_exists('flags',$i['message'])) && (in_array('private',$i['message']['flags']))) { - // This should not happen but until we can stop it... - logger('private message was delivered with no recipients.'); - continue; + if(array_key_exists('public_scope',$i['message']) && $i['message']['public_scope'] === 'public') { + // This should not happen but until we can stop it... + logger('private message was delivered with no recipients.'); + continue; + } } - logger('public post'); + logger('public post'); // Public post. look for any site members who are or may be accepting posts from this sender // and who are allowed to see them based on the sender's permissions $deliveries = allowed_public_recips($i); + // if the scope is anything but 'public' we're going to store it as private regardless + // of the private flag on the post. + + if($i['message'] && array_key_exists('public_scope',$i['message']) + && $i['message']['public_scope'] !== 'public') { + + if(! array_key_exists('flags',$i['message'])) + $i['message']['flags'] = array(); + if(! in_array('private',$i['message']['flags'])) + $i['message']['flags'][] = 'private'; + + } } // Go through the hash array and remove duplicates. array_unique() won't do this because the array is more than one level. @@ -1406,7 +1444,7 @@ function allowed_public_recips($msg) { $hash = make_xchan_hash($msg['notify']['sender']['guid'],$msg['notify']['sender']['guid_sig']); - if($scope === 'public' || $scope === 'network: red') + if($scope === 'public' || $scope === 'network: red' || $scope === 'authenticated') return $recips; if(strpos($scope,'site:') === 0) { @@ -1648,11 +1686,6 @@ function delete_imported_item($sender,$item,$uid) { require_once('include/items.php'); - // FIXME issue #230 is related - // Chicken/egg problem because we have to drop_item, but this removes information that tag_deliver may need to do its stuff. - // We can't reverse the order because drop_item refuses to run if the item already has the deleted flag set and we need to - // set that flag prior to calling tag_deliver. - // Use phased deletion to set the deleted flag, call both tag_deliver and the notifier to notify downstream channels // and then clean up after ourselves with a cron job after several days to do the delete_item_lowlevel() (DROPITEM_PHASE2). @@ -1997,6 +2030,7 @@ function import_site($arr,$pubkey) { $url = htmlspecialchars($arr['url'],ENT_COMPAT,'UTF-8',false); $sellpage = htmlspecialchars($arr['sellpage'],ENT_COMPAT,'UTF-8',false); $site_location = htmlspecialchars($arr['location'],ENT_COMPAT,'UTF-8',false); + $site_realm = htmlspecialchars($arr['realm'],ENT_COMPAT,'UTF-8',false); if($exists) { if(($siterecord['site_flags'] != $site_directory) @@ -2004,13 +2038,14 @@ function import_site($arr,$pubkey) { || ($siterecord['site_directory'] != $directory_url) || ($siterecord['site_sellpage'] != $sellpage) || ($siterecord['site_location'] != $site_location) - || ($siterecord['site_register'] != $register_policy)) { + || ($siterecord['site_register'] != $register_policy) + || ($siterecord['site_realm'] != $site_realm)) { $update = true; // logger('import_site: input: ' . print_r($arr,true)); // logger('import_site: stored: ' . print_r($siterecord,true)); - $r = q("update site set site_location = '%s', site_flags = %d, site_access = %d, site_directory = '%s', site_register = %d, site_update = '%s', site_sellpage = '%s' + $r = q("update site set site_location = '%s', site_flags = %d, site_access = %d, site_directory = '%s', site_register = %d, site_update = '%s', site_sellpage = '%s', site_realm = '%s' where site_url = '%s' limit 1", dbesc($site_location), intval($site_directory), @@ -2019,6 +2054,7 @@ function import_site($arr,$pubkey) { intval($register_policy), dbesc(datetime_convert()), dbesc($sellpage), + dbesc($site_realm), dbesc($url) ); if(! $r) { @@ -2028,8 +2064,8 @@ function import_site($arr,$pubkey) { } else { $update = true; - $r = q("insert into site ( site_location, site_url, site_access, site_flags, site_update, site_directory, site_register, site_sellpage ) - values ( '%s', '%s', %d, %d, '%s', '%s', %d, '%s' )", + $r = q("insert into site ( site_location, site_url, site_access, site_flags, site_update, site_directory, site_register, site_sellpage, site_realm ) + values ( '%s', '%s', %d, %d, '%s', '%s', %d, '%s', '%s' )", dbesc($site_location), dbesc($url), intval($access_policy), @@ -2037,7 +2073,8 @@ function import_site($arr,$pubkey) { dbesc(datetime_convert()), dbesc($directory_url), intval($register_policy), - dbesc($sellpage) + dbesc($sellpage), + dbesc($site_realm) ); if(! $r) { logger('import_site: record create failed. ' . print_r($arr,true)); @@ -2104,6 +2141,7 @@ function build_sync_packet($uid = 0, $packet = null, $groups_changed = false) { $info = (($packet) ? $packet : array()); $info['type'] = 'channel_sync'; + $info['encoding'] = 'red'; // note: not zot, this packet is very red specific if(array_key_exists($uid,$a->config) && array_key_exists('transient',$a->config[$uid])) { $settings = $a->config[$uid]['transient']; @@ -2234,6 +2272,21 @@ function process_channel_sync_delivery($sender,$arr,$deliveries) { $clean = array(); foreach($arr['abook'] as $abook) { + if($abook['abook_xchan'] && $abook['entry_deleted']) { + logger('process_channel_sync_delivery: removing abook entry for ' . $abook['abook_xchan']); + require_once('include/Contact.php'); + + $r = q("select abook_id from abook where abook_xchan = '%s' and abook_channel = %d and not ( abook_flags & %d ) limit 1", + dbesc($abook['abook_xchan']), + intval($channel['channel_id']), + intval(ABOOK_FLAG_SELF) + ); + if($r) + contact_remove($channel['channel_id'],$r[0]['abook_id']); + + continue; + } + // Perform discovery if the referenced xchan hasn't ever been seen on this hub. // This relies on the undocumented behaviour that red sites send xchan info with the abook |