diff options
Diffstat (limited to 'include')
32 files changed, 3899 insertions, 351 deletions
diff --git a/include/Scrape.php b/include/Scrape.php index 4f53effe9..b784650cd 100644 --- a/include/Scrape.php +++ b/include/Scrape.php @@ -432,7 +432,7 @@ function probe_url($url, $mode = PROBE_NORMAL) { intval(local_user()) ); if(count($x) && count($r)) { - $mailbox = construct_mailbox_name($r[0]); + $mailbox = construct_mailbox_name($r[0]); $password = ''; openssl_private_decrypt(hex2bin($r[0]['pass']),$password,$x[0]['prvkey']); $mbox = email_connect($mailbox,$r[0]['user'],$password); diff --git a/include/api.php b/include/api.php index b77156dfa..d790b4b87 100644 --- a/include/api.php +++ b/include/api.php @@ -565,18 +565,19 @@ if(requestdata('lat') && requestdata('long')) $_REQUEST['coord'] = sprintf("%s %s",requestdata('lat'),requestdata('long')); $_REQUEST['profile_uid'] = local_user(); - if(requestdata('parent')) + + if($parent) $_REQUEST['type'] = 'net-comment'; else { $_REQUEST['type'] = 'wall'; - if(x($_FILES,'media')) { - // upload the image if we have one - $_REQUEST['hush']='yeah'; //tell wall_upload function to return img info instead of echo - require_once('mod/wall_upload.php'); - $media = wall_upload_post($a); - if(strlen($media)>0) - $_REQUEST['body'] .= "\n\n".$media; - } + if(x($_FILES,'media')) { + // upload the image if we have one + $_REQUEST['hush']='yeah'; //tell wall_upload function to return img info instead of echo + require_once('mod/wall_upload.php'); + $media = wall_upload_post($a); + if(strlen($media)>0) + $_REQUEST['body'] .= "\n\n".$media; + } } // set this so that the item_post() function is quiet and doesn't redirect or emit json @@ -864,8 +865,13 @@ logger('API: api_statuses_show: '.$id); //$include_entities = (x($_REQUEST,'include_entities')?$_REQUEST['include_entities']:false); - //$sql_extra = ""; - if ($_GET["conversation"] == "true") $sql_extra .= " AND `item`.`parent` = %d ORDER BY `received` ASC "; else $sql_extra .= " AND `item`.`id` = %d"; + $conversation = (x($_REQUEST,'conversation')?1:0); + + $sql_extra = ''; + if ($conversation) + $sql_extra .= " AND `item`.`parent` = %d ORDER BY `received` ASC "; + else + $sql_extra .= " AND `item`.`id` = %d"; $r = q("SELECT `item`.*, `item`.`id` AS `item_id`, `contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`, @@ -875,16 +881,15 @@ WHERE `item`.`visible` = 1 and `item`.`moderated` = 0 AND `item`.`deleted` = 0 AND `contact`.`id` = `item`.`contact-id` AND `contact`.`blocked` = 0 AND `contact`.`pending` = 0 - $sql_extra - ", + $sql_extra", intval($id) ); -//var_dump($r); + $ret = api_format_items($r,$user_info); -//var_dump($ret); - if ($_GET["conversation"] == "true") { + + if ($conversation) { $data = array('$statuses' => $ret); - return api_apply_template("timeline", $type, $data); + return api_apply_template("timeline", $type, $data); } else { $data = array('$status' => $ret[0]); /*switch($type){ @@ -1233,6 +1238,40 @@ return($as); } + function api_format_messages($item, $recipient, $sender) { + // standard meta information + $ret=Array( + 'id' => $item['id'], + 'created_at' => api_date($item['created']), + 'sender_id' => $sender['id'] , + 'sender_screen_name' => $sender['screen_name'], + 'sender' => $sender, + 'recipient_id' => $recipient['id'], + 'recipient_screen_name' => $recipient['screen_name'], + 'recipient' => $recipient, + ); + + //don't send title to regular StatusNET requests to avoid confusing these apps + if (x($_GET, 'getText')) { + $ret['title'] = $item['title'] ; + if ($_GET["getText"] == "html") { + $ret['text'] = bbcode($item['body']); + } + elseif ($_GET["getText"] == "plain") { + $ret['text'] = html2plain(bbcode($item['body']), 0); + } + } + else { + $ret['text'] = $item['title']."\n".html2plain(bbcode($item['body']), 0); + } + if (isset($_GET["getUserObjects"]) && $_GET["getUserObjects"] == "false") { + unset($ret['sender']); + unset($ret['recipient']); + } + + return $ret; + } + function api_format_items($r,$user_info) { //logger('api_format_items: ' . print_r($r,true)); @@ -1429,7 +1468,13 @@ 'logo' => $logo, 'fancy' => 'true', 'language' => 'en', 'email' => $email, 'broughtby' => '', 'broughtbyurl' => '', 'timezone' => 'UTC', 'closed' => $closed, 'inviteonly' => 'false', 'private' => $private, 'textlimit' => $textlimit, 'sslserver' => $sslserver, 'ssl' => $ssl, - 'shorturllength' => '30' + 'shorturllength' => '30', + 'friendica' => array( + 'FRIENDICA_PLATFORM' => FRIENDICA_PLATFORM, + 'FRIENDICA_VERSION' => FRIENDICA_VERSION, + 'DFRN_PROTOCOL_VERSION' => DFRN_PROTOCOL_VERSION, + 'DB_UPDATE_VERSION' => DB_UPDATE_VERSION + ) ), ); @@ -1503,37 +1548,39 @@ if (local_user()===false) return false; if (!x($_POST, "text") || !x($_POST,"screen_name")) return; - + $sender = api_get_user($a); + require_once("include/message.php"); + $r = q("SELECT `id` FROM `contact` WHERE `uid`=%d AND `nick`='%s'", intval(local_user()), dbesc($_POST['screen_name'])); - + $recipient = api_get_user($a, $r[0]['id']); - + $replyto = ''; + $sub = ''; + if (x($_REQUEST,'replyto')) { + $r = q('SELECT `parent-uri`, `title` FROM `mail` WHERE `uid`=%d AND `id`=%d', + intval(local_user()), + intval($_REQUEST['replyto'])); + $replyto = $r[0]['parent-uri']; + $sub = $r[0]['title']; + } + else { + if (x($_REQUEST,'title')) { + $sub = $_REQUEST['title']; + } + else { + $sub = ((strlen($_POST['text'])>10)?substr($_POST['text'],0,10)."...":$_POST['text']); + } + } + + $id = send_message($recipient['id'], $_POST['text'], $sub, $replyto); - require_once("include/message.php"); - $sub = ( (strlen($_POST['text'])>10)?substr($_POST['text'],0,10)."...":$_POST['text']); - $id = send_message($recipient['id'], $_POST['text'], $sub); - - if ($id>-1) { $r = q("SELECT * FROM `mail` WHERE id=%d", intval($id)); - $item = $r[0]; - $ret=Array( - 'id' => $item['id'], - 'created_at'=> api_date($item['created']), - 'sender_id'=> $sender['id'] , - 'sender_screen_name'=> $sender['screen_name'], - 'sender'=> $sender, - 'recipient_id'=> $recipient['id'], - 'recipient_screen_name'=> $recipient['screen_name'], - 'recipient'=> $recipient, - - 'text'=> $item['title']."\n".html2plain(bbcode($item['body']), 0) , - - ); + $ret = api_format_messages($r[0], $recipient, $sender); } else { $ret = array("error"=>$id); @@ -1552,7 +1599,7 @@ } api_register_func('api/direct_messages/new','api_direct_messages_new',true); - function api_direct_messages_box(&$a, $type, $box) { + function api_direct_messages_box(&$a, $type, $box) { if (local_user()===false) return false; $user_info = api_get_user($a); @@ -1564,46 +1611,37 @@ $start = $page*$count; - + $profile_url = $a->get_baseurl() . '/profile/' . $a->user['nickname']; if ($box=="sentbox") { - $sql_extra = "`from-url`='%s'"; - } else { - $sql_extra = "`from-url`!='%s'"; + $sql_extra = "`from-url`='".dbesc( $profile_url )."'"; + } + elseif ($box=="conversation") { + $sql_extra = "`parent-uri`='".dbesc( $_GET["uri"] ) ."'"; + } + elseif ($box=="all") { + $sql_extra = "true"; + } + elseif ($box=="inbox") { + $sql_extra = "`from-url`!='".dbesc( $profile_url )."'"; } $r = q("SELECT * FROM `mail` WHERE uid=%d AND $sql_extra ORDER BY created DESC LIMIT %d,%d", intval(local_user()), - dbesc( $a->get_baseurl() . '/profile/' . $a->user['nickname'] ), intval($start), intval($count) - ); + ); $ret = Array(); - foreach($r as $item){ - switch ($box){ - case "inbox": - $recipient = $user_info; - $sender = api_get_user($a,$item['contact-id']); - break; - case "sentbox": - $recipient = api_get_user($a,$item['contact-id']); - $sender = $user_info; - break; + foreach($r as $item) { + if ($box == "inbox" || $item['from-url'] != $profile_url){ + $recipient = $user_info; + $sender = api_get_user($a,$item['contact-id']); } - - $ret[]=Array( - 'id' => $item['id'], - 'created_at'=> api_date($item['created']), - 'sender_id'=> $sender['id'] , - 'sender_screen_name'=> $sender['screen_name'], - 'sender'=> $sender, - 'recipient_id'=> $recipient['id'], - 'recipient_screen_name'=> $recipient['screen_name'], - 'recipient'=> $recipient, - - 'text'=> $item['title']."\n".html2plain(bbcode($item['body']), 0) , - - ); - + 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); } @@ -1624,6 +1662,14 @@ function api_direct_messages_inbox(&$a, $type){ return api_direct_messages_box($a, $type, "inbox"); } + function api_direct_messages_all(&$a, $type){ + return api_direct_messages_box($a, $type, "all"); + } + function api_direct_messages_conversation(&$a, $type){ + return api_direct_messages_box($a, $type, "conversation"); + } + api_register_func('api/direct_messages/conversation','api_direct_messages_conversation',true); + api_register_func('api/direct_messages/all','api_direct_messages_all',true); api_register_func('api/direct_messages/sent','api_direct_messages_sentbox',true); api_register_func('api/direct_messages','api_direct_messages_inbox',true); diff --git a/include/bb2diaspora.php b/include/bb2diaspora.php index d86ba4543..042177fd9 100644 --- a/include/bb2diaspora.php +++ b/include/bb2diaspora.php @@ -1,10 +1,11 @@ <?php require_once("include/oembed.php"); -require_once('include/event.php'); - -require_once('library/markdown.php'); -require_once('include/html2bbcode.php'); +require_once("include/event.php"); +require_once("library/markdown.php"); +require_once("include/html2bbcode.php"); +require_once("include/bbcode.php"); +require_once("include/markdownify/markdownify.php"); // we don't want to support a bbcode specific markdown interpreter // and the markdown library we have is pretty good, but provides HTML output. @@ -37,12 +38,15 @@ function diaspora2bb($s) { $s = Markdown($s); $s = str_replace('#','#',$s); - - $s = str_replace("\n",'<br />',$s); +// we seem to have double linebreaks +// $s = str_replace("\n",'<br />',$s); $s = html2bbcode($s); // $s = str_replace('*','*',$s); + // protect the recycle symbol from turning into a tag, but without unescaping angles and naked ampersands + $s = str_replace('♲',html_entity_decode('♲',ENT_QUOTES,'UTF-8'),$s); + // Convert everything that looks like a link to a link $s = preg_replace("/([^\]\=]|^)(https?\:\/\/)([a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", '$1[url=$2$3]$2$3[/url]',$s); @@ -66,9 +70,124 @@ function stripdcode_br_cb($s) { } +////////////////////// +// The following "diaspora_ul" and "diaspora_ol" are only appropriate for the +// pre-Markdownify conversion. If Markdownify isn't used, use the non-Markdownify +// versions below +////////////////////// +/* +function diaspora_ul($s) { + // Replace "[*]" followed by any number (including zero) of + // spaces by "* " to match Diaspora's list format + if( strpos($s[0], "[list]") === 0 ) + return '<ul class="listbullet" style="list-style-type: circle;">' . preg_replace("/\[\*\]( *)/", "* ", $s[1]) . '</ul>'; + elseif( strpos($s[0], "[ul]") === 0 ) + return '<ul class="listbullet" style="list-style-type: circle;">' . preg_replace("/\[\*\]( *)/", "* ", $s[1]) . '</ul>'; + else + return $s[0]; +} + + +function diaspora_ol($s) { + // A hack: Diaspora will create a properly-numbered ordered list even + // if you use '1.' for each element of the list, like: + // 1. First element + // 1. Second element + // 1. Third element + if( strpos($s[0], "[list=1]") === 0 ) + return '<ul class="listdecimal" style="list-style-type: decimal;">' . preg_replace("/\[\*\]( *)/", "1. ", $s[1]) . '</ul>'; + elseif( strpos($s[0], "[list=i]") === 0 ) + return '<ul class="listlowerroman" style="list-style-type: lower-roman;">' . preg_replace("/\[\*\]( *)/", "1. ", $s[1]) . '</ul>'; + elseif( strpos($s[0], "[list=I]") === 0 ) + return '<ul class="listupperroman" style="list-style-type: upper-roman;">' . preg_replace("/\[\*\]( *)/", "1. ", $s[1]) . '</ul>'; + elseif( strpos($s[0], "[list=a]") === 0 ) + return '<ul class="listloweralpha" style="list-style-type: lower-alpha;">' . preg_replace("/\[\*\]( *)/", "1. ", $s[1]) . '</ul>'; + elseif( strpos($s[0], "[list=A]") === 0 ) + return '<ul class="listupperalpha" style="list-style-type: upper-alpha;">' . preg_replace("/\[\*\]( *)/", "1. ", $s[1]) . '</ul>'; + elseif( strpos($s[0], "[ol]") === 0 ) + return '<ul class="listdecimal" style="list-style-type: decimal;">' . preg_replace("/\[\*\]( *)/", "1. ", $s[1]) . '</ul>'; + else + return $s[0]; +} +*/ + +////////////////////// +// Non-Markdownify versions of "diaspora_ol" and "diaspora_ul" +////////////////////// +function diaspora_ul($s) { + // Replace "[\\*]" followed by any number (including zero) of + // spaces by "* " to match Diaspora's list format + return preg_replace("/\[\\\\\*\]( *)/", "* ", $s[1]); +} + +function diaspora_ol($s) { + // A hack: Diaspora will create a properly-numbered ordered list even + // if you use '1.' for each element of the list, like: + // 1. First element + // 1. Second element + // 1. Third element + return preg_replace("/\[\\\\\*\]( *)/", "1. ", $s[1]); +} + function bb2diaspora($Text,$preserve_nl = false) { +////////////////////// +// An attempt was made to convert bbcode to html and then to markdown +// consisting of the following lines. +// I'm undoing this as we have a lot of bbcode constructs which +// were simply getting lost, for instance bookmark, vimeo, video, youtube, events, etc. +// We can try this again, but need a very good test sequence to verify +// all the major bbcode constructs that we use are getting through. +////////////////////// +/* + // bbcode() will convert "[*]" into "<li>" with no closing "</li>" + // Markdownify() is unable to handle these, as it makes each new + // "<li>" into a deeper nested element until it crashes. So pre-format + // the lists as Diaspora lists before sending the $Text to bbcode() + // + // Note that to get nested lists to work for Diaspora, we would need + // to define the closing tag for the list elements. So nested lists + // are going to be flattened out in Diaspora for now + + $endlessloop = 0; + while ((((strpos($Text, "[/list]") !== false) && (strpos($Text, "[list") !== false)) || + ((strpos($Text, "[/ol]") !== false) && (strpos($Text, "[ol]") !== false)) || + ((strpos($Text, "[/ul]") !== false) && (strpos($Text, "[ul]") !== false))) && (++$endlessloop < 20)) { + $Text = preg_replace_callback("/\[list\](.*?)\[\/list\]/is", 'diaspora_ul', $Text); + $Text = preg_replace_callback("/\[list=1\](.*?)\[\/list\]/is", 'diaspora_ol', $Text); + $Text = preg_replace_callback("/\[list=i\](.*?)\[\/list\]/s",'diaspora_ol', $Text); + $Text = preg_replace_callback("/\[list=I\](.*?)\[\/list\]/s", 'diaspora_ol', $Text); + $Text = preg_replace_callback("/\[list=a\](.*?)\[\/list\]/s", 'diaspora_ol', $Text); + $Text = preg_replace_callback("/\[list=A\](.*?)\[\/list\]/s", 'diaspora_ol', $Text); + $Text = preg_replace_callback("/\[ul\](.*?)\[\/ul\]/is", 'diaspora_ul', $Text); + $Text = preg_replace_callback("/\[ol\](.*?)\[\/ol\]/is", 'diaspora_ol', $Text); + } + +*/ + + // Convert it to HTML - don't try oembed +// $Text = bbcode($Text, $preserve_nl, false); + + // Now convert HTML to Markdown +// $md = new Markdownify(false, false, false); +// $Text = $md->parseString($Text); + + // If the text going into bbcode() has a plain URL in it, i.e. + // with no [url] tags around it, it will come out of parseString() + // looking like: <http://url.com>, which gets removed by strip_tags(). + // So take off the angle brackets of any such URL +// $Text = preg_replace("/<http(.*?)>/is", "http$1", $Text); + + // Remove all unconverted tags +// $Text = strip_tags($Text); + +////// +// end of bb->html->md conversion attempt +////// + + + $ev = bbtoevent($Text); // Replace any html brackets with HTML Entities to prevent executing HTML or script @@ -78,12 +197,17 @@ function bb2diaspora($Text,$preserve_nl = false) { $Text = str_replace(">", ">", $Text); // If we find any event code, turn it into an event. - // After we're finished processing the bbcode we'll + // After we're finished processing the bbcode we'll // replace all of the event code with a reformatted version. - if($preserve_nl) $Text = str_replace(array("\n","\r"), array('',''),$Text); + else + // Remove the "return" character, as Diaspora uses only the "newline" + // character, so having the "return" character can cause signature + // failures + $Text = str_replace("\r", "", $Text); + // Set up the parameters for a URL search string $URLSearchString = "^\[\]"; @@ -98,7 +222,7 @@ function bb2diaspora($Text,$preserve_nl = false) { // new javascript markdown processor to handle links with images as the link "text" // It is not optimal and may be removed if this ability is restored in the future - $Text = preg_replace("/\[url\=([$URLSearchString]*)\]\[img\](.*?)\[\/img\]\[\/url\]/ism", + $Text = preg_replace("/\[url\=([$URLSearchString]*)\]\[img\](.*?)\[\/img\]\[\/url\]/ism", '![' . t('image/photo') . '](' . '$2' . ')' . "\n" . '[' . t('link') . '](' . '$1' . ')', $Text); $Text = preg_replace("/\[bookmark\]([$URLSearchString]*)\[\/bookmark\]/ism", '[$1]($1)', $Text); @@ -115,7 +239,7 @@ function bb2diaspora($Text,$preserve_nl = false) { // Perform MAIL Search $Text = preg_replace("(\[mail\]([$MAILSearchString]*)\[/mail\])", '[$1](mailto:$1)', $Text); $Text = preg_replace("/\[mail\=([$MAILSearchString]*)\](.*?)\[\/mail\]/", '[$2](mailto:$1)', $Text); - + $Text = str_replace('*', '\\*', $Text); $Text = str_replace('_', '\\_', $Text); @@ -124,48 +248,61 @@ function bb2diaspora($Text,$preserve_nl = false) { // Check for bold text $Text = preg_replace("(\[b\](.*?)\[\/b\])is",'**$1**',$Text); - // Check for Italics text + // Check for italics text $Text = preg_replace("(\[i\](.*?)\[\/i\])is",'_$1_',$Text); - // Check for Underline text -// $Text = preg_replace("(\[u\](.*?)\[\/u\])is",'<u>$1</u>',$Text); + // Check for underline text + // Replace with italics since Diaspora doesn't have underline + $Text = preg_replace("(\[u\](.*?)\[\/u\])is",'_$1_',$Text); // Check for strike-through text -// $Text = preg_replace("(\[s\](.*?)\[\/s\])is",'<strike>$1</strike>',$Text); + $Text = preg_replace("(\[s\](.*?)\[\/s\])is",'**[strike]**$1**[/strike]**',$Text); // Check for over-line text // $Text = preg_replace("(\[o\](.*?)\[\/o\])is",'<span class="overline">$1</span>',$Text); // Check for colored text -// $Text = preg_replace("(\[color=(.*?)\](.*?)\[\/color\])is","<span style=\"color: $1;\">$2</span>",$Text); + // Remove color since Diaspora doesn't support it + $Text = preg_replace("(\[color=(.*?)\](.*?)\[\/color\])is","$2",$Text); // Check for sized text -// $Text = preg_replace("(\[size=(.*?)\](.*?)\[\/size\])is","<span style=\"font-size: $1;\">$2</span>",$Text); + // Remove it since Diaspora doesn't support sizes very well + $Text = preg_replace("(\[size=(.*?)\](.*?)\[\/size\])is","$2",$Text); // Check for list text -// $Text = preg_replace("/\[list\](.*?)\[\/list\]/is", '<ul class="listbullet">$1</ul>' ,$Text); -// $Text = preg_replace("/\[list=1\](.*?)\[\/list\]/is", '<ul class="listdecimal">$1</ul>' ,$Text); -// $Text = preg_replace("/\[list=i\](.*?)\[\/list\]/s",'<ul class="listlowerroman">$1</ul>' ,$Text); -// $Text = preg_replace("/\[list=I\](.*?)\[\/list\]/s", '<ul class="listupperroman">$1</ul>' ,$Text); -// $Text = preg_replace("/\[list=a\](.*?)\[\/list\]/s", '<ul class="listloweralpha">$1</ul>' ,$Text); -// $Text = preg_replace("/\[list=A\](.*?)\[\/list\]/s", '<ul class="listupperalpha">$1</ul>' ,$Text); -// $Text = preg_replace("/\[li\](.*?)\[\/li\]/s", '<li>$1</li>' ,$Text); - -// $Text = preg_replace("/\[td\](.*?)\[\/td\]/s", '<td>$1</td>' ,$Text); -// $Text = preg_replace("/\[tr\](.*?)\[\/tr\]/s", '<tr>$1</tr>' ,$Text); -// $Text = preg_replace("/\[table\](.*?)\[\/table\]/s", '<table>$1</table>' ,$Text); - -// $Text = preg_replace("/\[table border=1\](.*?)\[\/table\]/s", '<table border="1" >$1</table>' ,$Text); + $endlessloop = 0; + while ((((strpos($Text, "[/list]") !== false) && (strpos($Text, "[list") !== false)) || + ((strpos($Text, "[/ol]") !== false) && (strpos($Text, "[ol]") !== false)) || + ((strpos($Text, "[/ul]") !== false) && (strpos($Text, "[ul]") !== false)) || + ((strpos($Text, "[/li]") !== false) && (strpos($Text, "[li]") !== false))) && (++$endlessloop < 20)) { + $Text = preg_replace_callback("/\[list\](.*?)\[\/list\]/is", 'diaspora_ul', $Text); + $Text = preg_replace_callback("/\[list=1\](.*?)\[\/list\]/is", 'diaspora_ol', $Text); + $Text = preg_replace_callback("/\[list=i\](.*?)\[\/list\]/s",'diaspora_ol', $Text); + $Text = preg_replace_callback("/\[list=I\](.*?)\[\/list\]/s", 'diaspora_ol', $Text); + $Text = preg_replace_callback("/\[list=a\](.*?)\[\/list\]/s", 'diaspora_ol', $Text); + $Text = preg_replace_callback("/\[list=A\](.*?)\[\/list\]/s", 'diaspora_ol', $Text); + $Text = preg_replace_callback("/\[ul\](.*?)\[\/ul\]/is", 'diaspora_ul', $Text); + $Text = preg_replace_callback("/\[ol\](.*?)\[\/ol\]/is", 'diaspora_ol', $Text); + $Text = preg_replace("/\[li\]( *)(.*?)\[\/li\]/s", '* $2' ,$Text); + } + + // Just get rid of table tags since Diaspora doesn't support tables + $Text = preg_replace("/\[th\](.*?)\[\/th\]/s", '$1' ,$Text); + $Text = preg_replace("/\[td\](.*?)\[\/td\]/s", '$1' ,$Text); + $Text = preg_replace("/\[tr\](.*?)\[\/tr\]/s", '$1' ,$Text); + $Text = preg_replace("/\[table\](.*?)\[\/table\]/s", '$1' ,$Text); + + $Text = preg_replace("/\[table border=(.*?)\](.*?)\[\/table\]/s", '$2' ,$Text); // $Text = preg_replace("/\[table border=0\](.*?)\[\/table\]/s", '<table border="0" >$1</table>' ,$Text); - + // $Text = str_replace("[*]", "<li>", $Text); // Check for font change text // $Text = preg_replace("(\[font=(.*?)\](.*?)\[\/font\])","<span style=\"font-family: $1;\">$2</span>",$Text); - $Text = preg_replace_callback("/\[code\](.*?)\[\/code\]/is",'stripdcode_br_cb',$Text); + $Text = preg_replace_callback("/\[code\](.*?)\[\/code\]/is",'stripdcode_br_cb',$Text); // Check for [code] text $Text = preg_replace("/(\[code\])+(.*?)(\[\/code\])+/is","\t$2\n", $Text); @@ -174,10 +311,11 @@ function bb2diaspora($Text,$preserve_nl = false) { // Declare the format for [quote] layout - // $QuoteLayout = '<blockquote>$1</blockquote>'; + // $QuoteLayout = '<blockquote>$1</blockquote>'; // Check for [quote] text $Text = preg_replace("/\[quote\](.*?)\[\/quote\]/is",">$1\n\n", $Text); - + $Text = preg_replace("/\[quote=(.*?)\](.*?)\[\/quote\]/is",">$2\n\n", $Text); + // Images // html5 video and audio @@ -187,13 +325,13 @@ function bb2diaspora($Text,$preserve_nl = false) { $Text = preg_replace("/\[audio\](.*?)\[\/audio\]/", '$1', $Text); // $Text = preg_replace("/\[iframe\](.*?)\[\/iframe\]/", '<iframe src="$1" width="425" height="350"><a href="$1">$1</a></iframe>', $Text); - + // [img=widthxheight]image source[/img] // $Text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/", '<img src="$3" style="height:{$2}px; width:{$1}px;" >', $Text); - $Text = preg_replace("/\[youtube\]https?:\/\/www.youtube.com\/watch\?v\=(.*?)\[\/youtube\]/ism",'http://www.youtube.com/watch?v=$1',$Text); - $Text = preg_replace("/\[youtube\]https?:\/\/www.youtube.com\/embed\/(.*?)\[\/youtube\]/ism",'http://www.youtube.com/watch?v=$1',$Text); - $Text = preg_replace("/\[youtube\]https?:\/\/youtu.be\/(.*?)\[\/youtube\]/ism",'http://www.youtube.com/watch?v=$1',$Text); + $Text = preg_replace("/\[youtube\]https?:\/\/www.youtube.com\/watch\?v\=(.*?)\[\/youtube\]/ism",'http://www.youtube.com/watch?v=$1',$Text); + $Text = preg_replace("/\[youtube\]https?:\/\/www.youtube.com\/embed\/(.*?)\[\/youtube\]/ism",'http://www.youtube.com/watch?v=$1',$Text); + $Text = preg_replace("/\[youtube\]https?:\/\/youtu.be\/(.*?)\[\/youtube\]/ism",'http://www.youtube.com/watch?v=$1',$Text); $Text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", 'http://www.youtube.com/watch?v=$1', $Text); $Text = preg_replace("/\[vimeo\]https?:\/\/player.vimeo.com\/video\/([0-9]+)(.*?)\[\/vimeo\]/ism",'http://vimeo.com/$1',$Text); @@ -211,9 +349,10 @@ function bb2diaspora($Text,$preserve_nl = false) { if(x($ev,'desc') && x($ev,'start')) { $sub = format_event_diaspora($ev); - - $Text = preg_replace("/\[event\-description\](.*?)\[\/event\-description\]/is",$sub,$Text); - $Text = preg_replace("/\[event\-start\](.*?)\[\/event\-start\]/is",'',$Text); + + $Text = preg_replace("/\[event\-summary\](.*?)\[\/event\-summary\]/is",'',$Text); + $Text = preg_replace("/\[event\-description\](.*?)\[\/event\-description\]/is",'',$Text); + $Text = preg_replace("/\[event\-start\](.*?)\[\/event\-start\]/is",$sub,$Text); $Text = preg_replace("/\[event\-finish\](.*?)\[\/event\-finish\]/is",'',$Text); $Text = preg_replace("/\[event\-location\](.*?)\[\/event\-location\]/is",'',$Text); $Text = preg_replace("/\[event\-adjust\](.*?)\[\/event\-adjust\]/is",'',$Text); @@ -222,7 +361,12 @@ function bb2diaspora($Text,$preserve_nl = false) { $Text = preg_replace("/\<(.*?)(src|href)=(.*?)\&\;(.*?)\>/ism",'<$1$2=$3&$4>',$Text); $Text = preg_replace_callback('/\[(.*?)\]\((.*?)\)/ism','unescape_underscores_in_links',$Text); - + + + // Remove any leading or trailing whitespace, as this will mess up + // the Diaspora signature verification and cause the item to disappear + $Text = trim($Text); + call_hooks('bb2diaspora',$Text); return $Text; @@ -244,7 +388,7 @@ function format_event_diaspora($ev) { $o = 'Friendica event notification:' . "\n"; - $o .= '**' . bb2diaspora($ev['desc']) . '**' . "\n"; + $o .= '**' . (($ev['summary']) ? bb2diaspora($ev['summary']) : bb2diaspora($ev['desc'])) . '**' . "\n"; $o .= t('Starts:') . ' ' . '[' . (($ev['adjust']) ? day_translate(datetime_convert('UTC', 'UTC', diff --git a/include/bbcode.php b/include/bbcode.php index efc362880..55a879497 100644 --- a/include/bbcode.php +++ b/include/bbcode.php @@ -50,7 +50,7 @@ function bb_unspacefy_and_trim($st) { // BBcode 2 HTML was written by WAY2WEB.net // extended to work with Mistpark/Friendica - Mike Macgirvin -function bbcode($Text,$preserve_nl = false) { +function bbcode($Text,$preserve_nl = false, $tryoembed = true) { $a = get_app(); @@ -93,11 +93,19 @@ function bbcode($Text,$preserve_nl = false) { // Convert new line chars to html <br /> tags - $Text = nl2br($Text); + // nlbr seems to be hopelessly messed up + // $Text = nl2br($Text); + + // We'll emulate it. + + $Text = str_replace("\r\n","\n", $Text); + $Text = str_replace(array("\r","\n"), array('<br />','<br />'), $Text); + if($preserve_nl) $Text = str_replace(array("\n","\r"), array('',''),$Text); + // Set up the parameters for a URL search string $URLSearchString = "^\[\]"; // Set up the parameters for a MAIL search string @@ -108,10 +116,14 @@ function bbcode($Text,$preserve_nl = false) { $Text = preg_replace("/([^\]\=]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", '$1<a href="$2" target="external-link">$2</a>', $Text); - $Text = preg_replace_callback("/\[bookmark\=([^\]]*)\].*?\[\/bookmark\]/ism",'tryoembed',$Text); + if ($tryoembed) + $Text = preg_replace_callback("/\[bookmark\=([^\]]*)\].*?\[\/bookmark\]/ism",'tryoembed',$Text); + $Text = preg_replace("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism",'[url=$1]$2[/url]',$Text); - $Text = preg_replace_callback("/\[url\]([$URLSearchString]*)\[\/url\]/ism",'tryoembed',$Text); + if ($tryoembed) + $Text = preg_replace_callback("/\[url\]([$URLSearchString]*)\[\/url\]/ism",'tryoembed',$Text); + $Text = preg_replace("/\[url\]([$URLSearchString]*)\[\/url\]/ism", '<a href="$1" target="external-link">$1</a>', $Text); $Text = preg_replace("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '<a href="$1" target="external-link">$2</a>', $Text); //$Text = preg_replace("/\[url\=([$URLSearchString]*)\]([$URLSearchString]*)\[\/url\]/ism", '<a href="$1" target="_blank">$2</a>', $Text); @@ -154,11 +166,14 @@ function bbcode($Text,$preserve_nl = false) { // Check for list text $Text = str_replace("[*]", "<li>", $Text); - $Text = preg_replace("/\[li\](.*?)\[\/li\]/ism", '<li>$1</li>' ,$Text); // handle nested lists $endlessloop = 0; - while ((strpos($Text, "[/list]") !== false) and (strpos($Text, "[list") !== false) and (++$endlessloop < 20)) { + + while ((((strpos($Text, "[/list]") !== false) && (strpos($Text, "[list") !== false)) || + ((strpos($Text, "[/ol]") !== false) && (strpos($Text, "[ol]") !== false)) || + ((strpos($Text, "[/ul]") !== false) && (strpos($Text, "[ul]") !== false)) || + ((strpos($Text, "[/li]") !== false) && (strpos($Text, "[li]") !== false))) && (++$endlessloop < 20)) { $Text = preg_replace("/\[list\](.*?)\[\/list\]/ism", '<ul class="listbullet" style="list-style-type: circle;">$1</ul>' ,$Text); $Text = preg_replace("/\[list=\](.*?)\[\/list\]/ism", '<ul class="listnone" style="list-style-type: none;">$1</ul>' ,$Text); $Text = preg_replace("/\[list=1\](.*?)\[\/list\]/ism", '<ul class="listdecimal" style="list-style-type: decimal;">$1</ul>' ,$Text); @@ -166,11 +181,11 @@ function bbcode($Text,$preserve_nl = false) { $Text = preg_replace("/\[list=((?-i)I)\](.*?)\[\/list\]/ism", '<ul class="listupperroman" style="list-style-type: upper-roman;">$2</ul>' ,$Text); $Text = preg_replace("/\[list=((?-i)a)\](.*?)\[\/list\]/ism", '<ul class="listloweralpha" style="list-style-type: lower-alpha;">$2</ul>' ,$Text); $Text = preg_replace("/\[list=((?-i)A)\](.*?)\[\/list\]/ism", '<ul class="listupperalpha" style="list-style-type: upper-alpha;">$2</ul>' ,$Text); + $Text = preg_replace("/\[ul\](.*?)\[\/ul\]/ism", '<ul class="listbullet" style="list-style-type: circle;">$1</ul>' ,$Text); + $Text = preg_replace("/\[ol\](.*?)\[\/ol\]/ism", '<ul class="listdecimal" style="list-style-type: decimal;">$1</ul>' ,$Text); + $Text = preg_replace("/\[li\](.*?)\[\/li\]/ism", '<li>$1</li>' ,$Text); } - $Text = preg_replace("/\[ul\](.*?)\[\/ul\]/ism", '<ul class="listbullet" style="list-style-type: circle;">$1</ul>' ,$Text); - $Text = preg_replace("/\[ol\](.*?)\[\/ol\]/ism", '<ul class="listdecimal" style="list-style-type: decimal;">$1</ul>' ,$Text); - $Text = preg_replace("/\[th\](.*?)\[\/th\]/sm", '<th>$1</th>' ,$Text); $Text = preg_replace("/\[td\](.*?)\[\/td\]/sm", '<td>$1</td>' ,$Text); $Text = preg_replace("/\[tr\](.*?)\[\/tr\]/sm", '<tr>$1</tr>' ,$Text); @@ -190,7 +205,7 @@ function bbcode($Text,$preserve_nl = false) { // Declare the format for [code] layout - $Text = preg_replace_callback("/\[code\](.*?)\[\/code\]/ism",'stripcode_br_cb',$Text); +// $Text = preg_replace_callback("/\[code\](.*?)\[\/code\]/ism",'stripcode_br_cb',$Text); $CodeLayout = '<code>$1</code>'; // Check for [code] text @@ -250,31 +265,35 @@ function bbcode($Text,$preserve_nl = false) { $Text = preg_replace("/\[audio\](.*?\.(ogg|ogv|oga|ogm|webm|mp4|mp3))\[\/audio\]/ism", '<audio src="$1" controls="controls"><a href="$1">$1</a></audio>', $Text); // Try to Oembed - $Text = preg_replace_callback("/\[video\](.*?)\[\/video\]/ism", 'tryoembed', $Text); - $Text = preg_replace_callback("/\[audio\](.*?)\[\/audio\]/ism", 'tryoembed', $Text); - + if ($tryoembed) { + $Text = preg_replace_callback("/\[video\](.*?)\[\/video\]/ism", 'tryoembed', $Text); + $Text = preg_replace_callback("/\[audio\](.*?)\[\/audio\]/ism", 'tryoembed', $Text); + } // html5 video and audio $Text = preg_replace("/\[iframe\](.*?)\[\/iframe\]/ism", '<iframe src="$1" width="425" height="350"><a href="$1">$1</a></iframe>', $Text); - + // Youtube extensions - $Text = preg_replace_callback("/\[youtube\](https?:\/\/www.youtube.com\/watch\?v\=.*?)\[\/youtube\]/ism", 'tryoembed', $Text); - $Text = preg_replace_callback("/\[youtube\](www.youtube.com\/watch\?v\=.*?)\[\/youtube\]/ism", 'tryoembed', $Text); - $Text = preg_replace_callback("/\[youtube\](https?:\/\/youtu.be\/.*?)\[\/youtube\]/ism",'tryoembed',$Text); - + if ($tryoembed) { + $Text = preg_replace_callback("/\[youtube\](https?:\/\/www.youtube.com\/watch\?v\=.*?)\[\/youtube\]/ism", 'tryoembed', $Text); + $Text = preg_replace_callback("/\[youtube\](www.youtube.com\/watch\?v\=.*?)\[\/youtube\]/ism", 'tryoembed', $Text); + $Text = preg_replace_callback("/\[youtube\](https?:\/\/youtu.be\/.*?)\[\/youtube\]/ism",'tryoembed',$Text); + } + $Text = preg_replace("/\[youtube\]https?:\/\/www.youtube.com\/watch\?v\=(.*?)\[\/youtube\]/ism",'[youtube]$1[/youtube]',$Text); $Text = preg_replace("/\[youtube\]https?:\/\/www.youtube.com\/embed\/(.*?)\[\/youtube\]/ism",'[youtube]$1[/youtube]',$Text); - $Text = preg_replace("/\[youtube\]https?:\/\/youtu.be\/(.*?)\[\/youtube\]/ism",'[youtube]$1[/youtube]',$Text); - - + $Text = preg_replace("/\[youtube\]https?:\/\/youtu.be\/(.*?)\[\/youtube\]/ism",'[youtube]$1[/youtube]',$Text); + $Text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", '<iframe width="425" height="350" src="http://www.youtube.com/embed/$1" frameborder="0" ></iframe>', $Text); - $Text = preg_replace_callback("/\[vimeo\](https?:\/\/player.vimeo.com\/video\/[0-9]+).*?\[\/vimeo\]/ism",'tryoembed',$Text); - $Text = preg_replace_callback("/\[vimeo\](https?:\/\/vimeo.com\/[0-9]+).*?\[\/vimeo\]/ism",'tryoembed',$Text); + if ($tryoembed) { + $Text = preg_replace_callback("/\[vimeo\](https?:\/\/player.vimeo.com\/video\/[0-9]+).*?\[\/vimeo\]/ism",'tryoembed',$Text); + $Text = preg_replace_callback("/\[vimeo\](https?:\/\/vimeo.com\/[0-9]+).*?\[\/vimeo\]/ism",'tryoembed',$Text); + } $Text = preg_replace("/\[vimeo\]https?:\/\/player.vimeo.com\/video\/([0-9]+)(.*?)\[\/vimeo\]/ism",'[vimeo]$1[/vimeo]',$Text); $Text = preg_replace("/\[vimeo\]https?:\/\/vimeo.com\/([0-9]+)(.*?)\[\/vimeo\]/ism",'[vimeo]$1[/vimeo]',$Text); @@ -287,12 +306,16 @@ function bbcode($Text,$preserve_nl = false) { $Text = oembed_bbcode2html($Text); // If we found an event earlier, strip out all the event code and replace with a reformatted version. + // Replace the event-start section with the entire formatted event. The other bbcode is stripped. + // Summary (e.g. title) is required, earlier revisions only required description (in addition to + // start which is always required). Allow desc with a missing summary for compatibility. - if(x($ev,'desc') && x($ev,'start')) { + if((x($ev,'desc') || x($ev,'summary')) && x($ev,'start')) { $sub = format_event_html($ev); - $Text = preg_replace("/\[event\-description\](.*?)\[\/event\-description\]/ism",$sub,$Text); - $Text = preg_replace("/\[event\-start\](.*?)\[\/event\-start\]/ism",'',$Text); + $Text = preg_replace("/\[event\-summary\](.*?)\[\/event\-summary\]/ism",'',$Text); + $Text = preg_replace("/\[event\-description\](.*?)\[\/event\-description\]/ism",'',$Text); + $Text = preg_replace("/\[event\-start\](.*?)\[\/event\-start\]/ism",$sub,$Text); $Text = preg_replace("/\[event\-finish\](.*?)\[\/event\-finish\]/ism",'',$Text); $Text = preg_replace("/\[event\-location\](.*?)\[\/event\-location\]/ism",'',$Text); $Text = preg_replace("/\[event\-adjust\](.*?)\[\/event\-adjust\]/ism",'',$Text); diff --git a/include/conversation.php b/include/conversation.php index 2244e8df7..bbb6a1f85 100644 --- a/include/conversation.php +++ b/include/conversation.php @@ -432,7 +432,7 @@ function conversation(&$a, $items, $mode, $update, $preview = false) { continue; $toplevelpost = (($item['id'] == $item['parent']) ? true : false); - $toplevelprivate = false; + // Take care of author collapsing and comment collapsing // (author collapsing is currently disabled) @@ -440,7 +440,7 @@ function conversation(&$a, $items, $mode, $update, $preview = false) { // If there are more than two comments, squash all but the last 2. if($toplevelpost) { - $toplevelprivate = (($toplevelpost && $item['private']) ? true : false); + $item_writeable = (($item['writable'] || $item['self']) ? true : false); $comments_seen = 0; @@ -485,7 +485,7 @@ function conversation(&$a, $items, $mode, $update, $preview = false) { $redirect_url = $a->get_baseurl($ssl_state) . '/redir/' . $item['cid'] ; - $lock = ((($item['private']) || (($item['uid'] == local_user()) && (strlen($item['allow_cid']) || strlen($item['allow_gid']) + $lock = ((($item['private'] == 1) || (($item['uid'] == local_user()) && (strlen($item['allow_cid']) || strlen($item['allow_gid']) || strlen($item['deny_cid']) || strlen($item['deny_gid'])))) ? t('Private Message') : false); @@ -546,16 +546,16 @@ function conversation(&$a, $items, $mode, $update, $preview = false) { } $likebuttons = ''; - $shareable = ((($profile_owner == local_user()) && ((! $item['private']) || $item['network'] === NETWORK_FEED)) ? true : false); + $shareable = ((($profile_owner == local_user()) && ($item['private'] != 1)) ? true : false); if($page_writeable) { - if($toplevelpost) { +/* if($toplevelpost) { */ $likebuttons = array( 'like' => array( t("I like this \x28toggle\x29"), t("like")), 'dislike' => array( t("I don't like this \x28toggle\x29"), t("dislike")), ); if ($shareable) $likebuttons['share'] = array( t('Share this'), t('share')); - } +/* } */ $qc = $qcomment = null; @@ -659,8 +659,8 @@ function conversation(&$a, $items, $mode, $update, $preview = false) { else $profile_avatar = (((strlen($item['author-avatar'])) && $diff_author) ? $item['author-avatar'] : $a->get_cached_avatar_image($thumb)); - $like = ((x($alike,$item['id'])) ? format_like($alike[$item['id']],$alike[$item['id'] . '-l'],'like',$item['id']) : ''); - $dislike = ((x($dlike,$item['id'])) ? format_like($dlike[$item['id']],$dlike[$item['id'] . '-l'],'dislike',$item['id']) : ''); + $like = ((x($alike,$item['uri'])) ? format_like($alike[$item['uri']],$alike[$item['uri'] . '-l'],'like',$item['uri']) : ''); + $dislike = ((x($dlike,$item['uri'])) ? format_like($dlike[$item['uri']],$dlike[$item['uri'] . '-l'],'dislike',$item['uri']) : ''); $locate = array('location' => $item['location'], 'coord' => $item['coord'], 'html' => ''); call_hooks('render_location',$locate); @@ -876,13 +876,17 @@ function like_puller($a,$item,&$arr,$mode) { } else $url = zrl($url); - if(! ((isset($arr[$item['parent'] . '-l'])) && (is_array($arr[$item['parent'] . '-l'])))) - $arr[$item['parent'] . '-l'] = array(); - if(! isset($arr[$item['parent']])) - $arr[$item['parent']] = 1; + + if(! $item['thr-parent']) + $item['thr-parent'] = $item['parent-uri']; + + if(! ((isset($arr[$item['thr-parent'] . '-l'])) && (is_array($arr[$item['thr-parent'] . '-l'])))) + $arr[$item['thr-parent'] . '-l'] = array(); + if(! isset($arr[$item['thr-parent']])) + $arr[$item['thr-parent']] = 1; else - $arr[$item['parent']] ++; - $arr[$item['parent'] . '-l'][] = '<a href="'. $url . '"'. $sparkle .'>' . $item['author-name'] . '</a>'; + $arr[$item['thr-parent']] ++; + $arr[$item['thr-parent'] . '-l'][] = '<a href="'. $url . '"'. $sparkle .'>' . $item['author-name'] . '</a>'; } return; }} diff --git a/include/crypto.php b/include/crypto.php index 6fc9a287e..ed0a35704 100644 --- a/include/crypto.php +++ b/include/crypto.php @@ -261,39 +261,6 @@ function aes_unencapsulate($data,$prvkey) { return AES256CBC_decrypt(base64url_decode($data['data']),$k,$i); } - -// This has been superceded. - -function zot_encapsulate($data,$envelope,$pubkey) { -$res = aes_encapsulate($data,$pubkey); - -return <<< EOT -<?xml version='1.0' encoding='UTF-8'?> -<zot:msg xmlns:zot='http://purl.org/zot/1.0'> - <zot:key>{$res['key']}</zot:key> - <zot:iv>{$res['iv']}</zot:iv> - <zot:env>$s1</zot:env> - <zot:sig key_id="$keyid">$sig</zot:sig> - <zot:alg>AES-256-CBC</zot:alg> - <zot:data type='application/magic-envelope+xml'>{$res['data']}</zot:data> -</zot:msg> -EOT; - -} - -// so has this - -function zot_unencapsulate($data,$prvkey) { - $ret = array(); - $c = array(); - $x = parse_xml_string($data); - $c = array('key' => $x->key,'iv' => $x->iv,'data' => $x->data); - openssl_private_decrypt(base64url_decode($x->sender),$s,$prvkey); - $ret['sender'] = $s; - $ret['data'] = aes_unencapsulate($x,$prvkey); - return $ret; -} - function new_keypair($bits) { $openssl_options = array( diff --git a/include/datetime.php b/include/datetime.php index 58a618610..75ae685f3 100644 --- a/include/datetime.php +++ b/include/datetime.php @@ -447,11 +447,13 @@ function update_contact_birthdays() { * */ - $bdtext = t('Birthday:') . ' [url=' . $rr['url'] . ']' . $rr['name'] . '[/url]' ; + $bdtext = sprintf( t('%s\'s birthday'), $rr['name']); + $bdtext2 = sprintf( t('Happy Birthday %s'), ' [url=' . $rr['url'] . ']' . $rr['name'] . '[/url]') ; - $r = q("INSERT INTO `event` (`uid`,`cid`,`created`,`edited`,`start`,`finish`,`desc`,`type`,`adjust`) - VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%d' ) ", + + $r = q("INSERT INTO `event` (`uid`,`cid`,`created`,`edited`,`start`,`finish`,`summary`,`desc`,`type`,`adjust`) + VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d' ) ", intval($rr['uid']), intval($rr['id']), dbesc(datetime_convert()), @@ -459,6 +461,7 @@ function update_contact_birthdays() { dbesc(datetime_convert('UTC','UTC', $nextbd)), dbesc(datetime_convert('UTC','UTC', $nextbd . ' + 1 day ')), dbesc($bdtext), + dbesc($bdtext2), dbesc('birthday'), intval(0) ); diff --git a/include/dba.php b/include/dba.php index 881097f30..71c1ba859 100644 --- a/include/dba.php +++ b/include/dba.php @@ -275,3 +275,9 @@ function dbesc_array(&$arr) { array_walk($arr,'dbesc_array_cb'); } }} + + +function dba_timer() { + return microtime(true); +} + diff --git a/include/delivery.php b/include/delivery.php index e6cfc8155..1328771a6 100644 --- a/include/delivery.php +++ b/include/delivery.php @@ -113,7 +113,7 @@ function delivery_run($argv, $argc){ $uid = $r[0]['uid']; $updated = $r[0]['edited']; - // The following seems superfluous. We've already checked for "if (! intval($r[0]['parent']))" a few lines up + // POSSIBLE CLEANUP --> The following seems superfluous. We've already checked for "if (! intval($r[0]['parent']))" a few lines up if(! $parent_id) continue; @@ -280,7 +280,7 @@ function delivery_run($argv, $argc){ continue; // private emails may be in included in public conversations. Filter them. - if(($public_message) && $item['private']) + if(($public_message) && $item['private'] == 1) continue; $item_contact = get_item_contact($item,$icontacts); @@ -383,7 +383,7 @@ function delivery_run($argv, $argc){ continue; // private emails may be in included in public conversations. Filter them. - if(($public_message) && $item['private']) + if(($public_message) && $item['private'] == 1) continue; $item_contact = get_item_contact($item,$icontacts); diff --git a/include/diaspora.php b/include/diaspora.php index 0ca9163a8..7551ea9b3 100755 --- a/include/diaspora.php +++ b/include/diaspora.php @@ -5,6 +5,7 @@ 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) { @@ -113,27 +114,93 @@ function diaspora_get_contact_by_handle($uid,$handle) { } function find_diaspora_person_by_handle($handle) { + + $person = false; $update = false; - $r = q("select * from fcontact where network = '%s' and addr = '%s' limit 1", - dbesc(NETWORK_DIASPORA), - dbesc($handle) - ); - if(count($r)) { - 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($r[0]['updated'] . ' +00:00'); - if($d > strtotime('now - 14 days')) - return $r[0]; - $update = true; - } - logger('find_diaspora_person_by_handle: refresh',LOGGER_DEBUG); - require_once('include/Scrape.php'); - $r = probe_url($handle, PROBE_DIASPORA); - if((count($r)) && ($r['network'] === NETWORK_DIASPORA)) { - add_fcontact($r,$update); - return ($r); - } - return false; + $got_lock = false; + + $endlessloop = 0; + $maxloops = 10; + + do { + $r = q("select * from fcontact where network = '%s' and addr = '%s' limit 1", + dbesc(NETWORK_DIASPORA), + dbesc($handle) + ); + if(count($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. + + 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; } @@ -844,7 +911,7 @@ function diaspora_reshare($importer,$xml) { else $details = $orig_author; - $prefix = '♲ ' . $details . "\n"; + $prefix = html_entity_decode("♲ ", ENT_QUOTES, 'UTF-8') . $details . "\n"; // allocate a guid on our system - we aren't fixing any collisions. @@ -1560,7 +1627,8 @@ function diaspora_photo($importer,$xml,$msg) { $link_text = '[img]' . $remote_photo_path . $remote_photo_name . '[/img]' . "\n"; - $link_text = scale_external_images($link_text); + $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 limit 1", @@ -2005,6 +2073,7 @@ function diaspora_share($me,$contact) { )); $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)); } @@ -2022,6 +2091,7 @@ function diaspora_unshare($me,$contact) { )); $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)); @@ -2060,13 +2130,21 @@ function diaspora_send_status($item,$owner,$contact,$public_batch = false) { $images[] = $detail; $body = str_replace($detail['str'],$mtch[1],$body); } - } + } */ + // Removal of tags + $body = preg_replace('/#\[url\=(\w+.*?)\](\w+.*?)\[\/url\]/i', '#$2', $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 = xmlify('**' . html_entity_decode($title) . '**' . "\n") . $body; - + $body = "## ".html_entity_decode($title)."\n\n".$body; if($item['attach']) { $cnt = preg_match_all('/href=\"(.*?)\"(.*?)title=\"(.*?)\"/ism',$item['attach'],$matches,PREG_SET_ORDER); @@ -2096,6 +2174,7 @@ function diaspora_send_status($item,$owner,$contact,$public_batch = false) { 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); @@ -2140,6 +2219,7 @@ function diaspora_send_images($item,$owner,$contact,$images,$public_batch = fals 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); } @@ -2203,6 +2283,7 @@ function diaspora_send_followup($item,$owner,$contact,$public_batch = false) { 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)); } @@ -2238,7 +2319,6 @@ function diaspora_send_relay($item,$owner,$contact,$public_batch = false) { $relay_retract = true; $target_type = ( ($item['verb'] === ACTIVITY_LIKE) ? 'Like' : 'Comment'); - $sender_signed_text = $item['guid'] . ';' . $target_type ; $sql_sign_id = 'retract_iid'; $tpl = get_markup_template('diaspora_relayable_retraction.tpl'); @@ -2249,13 +2329,10 @@ function diaspora_send_relay($item,$owner,$contact,$public_batch = false) { $target_type = 'Post'; // $positive = (($item['deleted']) ? 'false' : 'true'); $positive = 'true'; - $sender_signed_text = $item['guid'] . ';' . $target_type . ';' . $parent_guid . ';' . $positive . ';' . $myaddr; $tpl = get_markup_template('diaspora_like_relay.tpl'); } else { // item is a comment - $sender_signed_text = $item['guid'] . ';' . $parent_guid . ';' . $text . ';' . $myaddr; - $tpl = get_markup_template('diaspora_comment_relay.tpl'); } @@ -2281,6 +2358,13 @@ function diaspora_send_relay($item,$owner,$contact,$public_batch = false) { 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 @@ -2309,6 +2393,7 @@ function diaspora_send_relay($item,$owner,$contact,$public_batch = false) { $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)); @@ -2343,6 +2428,7 @@ function diaspora_send_retraction($item,$owner,$contact,$public_batch = false) { )); $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)); } @@ -2403,6 +2489,7 @@ function diaspora_send_mail($item,$owner,$contact) { 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)); diff --git a/include/enotify.php b/include/enotify.php index 134e42f8e..5e073bf3c 100644 --- a/include/enotify.php +++ b/include/enotify.php @@ -54,6 +54,20 @@ function notification($params) { $parent_id = $params['parent']; + // Check to see if there was already a tag notify for this post. + // If so don't create a second notification + + $p = null; + $p = q("select id from notify where type = %d and link = '%s' and uid = %d limit 1", + intval(NOTIFY_TAGSELF), + dbesc($params['link']), + intval($params['uid']) + ); + if($p and count($p)) { + pop_lang(); + return; + } + // if it's a post figure out who's post it is. diff --git a/include/event.php b/include/event.php index 866ae8c3f..8aef0a263 100644 --- a/include/event.php +++ b/include/event.php @@ -12,6 +12,9 @@ function format_event_html($ev) { $o = '<div class="vevent">' . "\r\n"; + + $o .= '<p class="summary event-summary">' . bbcode($ev['summary']) . '</p>' . "\r\n"; + $o .= '<p class="description event-description">' . bbcode($ev['desc']) . '</p>' . "\r\n"; $o .= '<p class="event-start">' . t('Starts:') . ' <abbr class="dtstart" title="' @@ -114,6 +117,9 @@ function format_event_bbcode($ev) { $o = ''; + if($ev['summary']) + $o .= '[event-summary]' . $ev['summary'] . '[/event-summary]'; + if($ev['desc']) $o .= '[event-description]' . $ev['desc'] . '[/event-description]'; @@ -148,6 +154,9 @@ function bbtoevent($s) { $ev = array(); $match = ''; + if(preg_match("/\[event\-summary\](.*?)\[\/event\-summary\]/is",$s,$match)) + $ev['summary'] = $match[1]; + $match = ''; if(preg_match("/\[event\-description\](.*?)\[\/event\-description\]/is",$s,$match)) $ev['desc'] = $match[1]; $match = ''; @@ -244,6 +253,7 @@ function event_store($arr) { `edited` = '%s', `start` = '%s', `finish` = '%s', + `summary` = '%s', `desc` = '%s', `location` = '%s', `type` = '%s', @@ -258,6 +268,7 @@ function event_store($arr) { dbesc($arr['edited']), dbesc($arr['start']), dbesc($arr['finish']), + dbesc($arr['summary']), dbesc($arr['desc']), dbesc($arr['location']), dbesc($arr['type']), @@ -306,9 +317,9 @@ function event_store($arr) { // New event. Store it. - $r = q("INSERT INTO `event` ( `uid`,`cid`,`uri`,`created`,`edited`,`start`,`finish`,`desc`,`location`,`type`, + $r = q("INSERT INTO `event` ( `uid`,`cid`,`uri`,`created`,`edited`,`start`,`finish`,`summary`, `desc`,`location`,`type`, `adjust`,`nofinish`,`allow_cid`,`allow_gid`,`deny_cid`,`deny_gid`) - VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', '%s', '%s' ) ", + VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', '%s', '%s' ) ", intval($arr['uid']), intval($arr['cid']), dbesc($arr['uri']), @@ -316,6 +327,7 @@ function event_store($arr) { dbesc($arr['edited']), dbesc($arr['start']), dbesc($arr['finish']), + dbesc($arr['summary']), dbesc($arr['desc']), dbesc($arr['location']), dbesc($arr['type']), diff --git a/include/follow.php b/include/follow.php index 22288a0da..b4d1732b8 100644 --- a/include/follow.php +++ b/include/follow.php @@ -62,6 +62,11 @@ function new_contact($uid,$url,$interactive = false) { } } + + + + + // This extra param just confuses things, remove it if($ret['network'] === NETWORK_DIASPORA) $ret['url'] = str_replace('?absolute=true','',$ret['url']); @@ -89,6 +94,11 @@ function new_contact($uid,$url,$interactive = false) { $ret['notify'] = ''; } + + + + + if(! $ret['notify']) { $result['message'] .= t('Limited profile. This person will be unable to receive direct/personal notifications from you.') . EOL; } @@ -129,6 +139,32 @@ function new_contact($uid,$url,$interactive = false) { } else { + + // check service class limits + + $r = q("select count(*) as total from contact where uid = %d and pending = 0 and self = 0", + intval($uid) + ); + if(count($r)) + $total_contacts = $r[0]['total']; + + if(! service_class_allows($uid,'total_contacts',$total_contacts)) { + $result['message'] .= upgrade_message(); + return $result; + } + + $r = q("select count(network) as total from contact where uid = %d and network = '%s' and pending = 0 and self = 0", + intval($uid), + dbesc($network) + ); + if(count($r)) + $total_network = $r[0]['total']; + + if(! service_class_allows($uid,'total_contacts_' . $network,$total_network)) { + $result['message'] .= upgrade_message(); + return $result; + } + $new_relation = (($ret['network'] === NETWORK_MAIL) ? CONTACT_IS_FRIEND : CONTACT_IS_SHARING); if($ret['network'] === NETWORK_DIASPORA) $new_relation = CONTACT_IS_FOLLOWER; diff --git a/include/html2bbcode.php b/include/html2bbcode.php index 69ccf41b7..985c36eaa 100644 --- a/include/html2bbcode.php +++ b/include/html2bbcode.php @@ -124,7 +124,7 @@ function html2bbcode($message) $node->nodeValue = str_replace("\n", "\r", $node->nodeValue); $message = $doc->saveHTML(); - $message = str_replace(array("\n<", ">\n", "\r", "\n", "\xC3\x82\xC2\xA0"), array("<", ">", "<br>", " ", ""), $message); + $message = str_replace(array("\n<", ">\n", "\r", "\n", "\xC3\x82\xC2\xA0"), array("<", ">", "<br />", " ", ""), $message); $message = preg_replace('= [\s]*=i', " ", $message); @$doc->loadHTML($message); diff --git a/include/items.php b/include/items.php index a0dd1c815..cf903ac13 100755 --- a/include/items.php +++ b/include/items.php @@ -466,8 +466,8 @@ function get_atom_elements($feed,$item) { $res['last-child'] = 0; $private = $item->get_item_tags(NAMESPACE_DFRN,'private'); - if($private && $private[0]['data'] == 1) - $res['private'] = 1; + if($private && intval($private[0]['data']) > 0) + $res['private'] = intval($private[0]['data']); else $res['private'] = 0; @@ -814,7 +814,7 @@ function item_store($arr,$force_parent = false) { // email correspondents to be private even if the overall thread is not. if($r[0]['private']) - $arr['private'] = 1; + $arr['private'] = $r[0]['private']; // Edge case. We host a public forum that was originally posted to privately. // The original author commented, but as this is a comment, the permissions @@ -1457,11 +1457,12 @@ function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) * */ - $bdtext = t('Birthday:') . ' [url=' . $contact['url'] . ']' . $contact['name'] . '[/url]' ; + $bdtext = sprintf( t('%s\'s birthday'), $contact['name']); + $bdtext2 = sprintf( t('Happy Birthday %s'), ' [url=' . $contact['url'] . ']' . $contact['name'] . '[/url]' ) ; - $r = q("INSERT INTO `event` (`uid`,`cid`,`created`,`edited`,`start`,`finish`,`desc`,`type`) - VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s' ) ", + $r = q("INSERT INTO `event` (`uid`,`cid`,`created`,`edited`,`start`,`finish`,`summary`,`desc`,`type`) + VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) ", intval($contact['uid']), intval($contact['id']), dbesc(datetime_convert()), @@ -1469,6 +1470,7 @@ function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) dbesc(datetime_convert('UTC','UTC', $birthday)), dbesc(datetime_convert('UTC','UTC', $birthday . ' + 1 day ')), dbesc($bdtext), + dbesc($bdtext2), dbesc('birthday') ); @@ -1658,6 +1660,21 @@ function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) continue; } + $force_parent = false; + if($contact['network'] === NETWORK_OSTATUS || stristr($contact['url'],'twitter.com')) { + if($contact['network'] === NETWORK_OSTATUS) + $force_parent = true; + if(strlen($datarray['title'])) + unset($datarray['title']); + $r = q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d", + dbesc(datetime_convert()), + dbesc($parent_uri), + intval($importer['uid']) + ); + $datarray['last-child'] = 1; + } + + $r = q("SELECT `uid`, `last-child`, `edited`, `body` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($item_id), intval($importer['uid']) @@ -1701,19 +1718,6 @@ function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) continue; } - $force_parent = false; - if($contact['network'] === NETWORK_OSTATUS || stristr($contact['url'],'twitter.com')) { - if($contact['network'] === NETWORK_OSTATUS) - $force_parent = true; - if(strlen($datarray['title'])) - unset($datarray['title']); - $r = q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d", - dbesc(datetime_convert()), - dbesc($parent_uri), - intval($importer['uid']) - ); - $datarray['last-child'] = 1; - } if(($contact['network'] === NETWORK_FEED) || (! strlen($contact['notify']))) { // one way feed - no remote comment ability @@ -1726,10 +1730,12 @@ function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) $datarray['type'] = 'activity'; $datarray['gravity'] = GRAVITY_LIKE; // only one like or dislike per person - $r = q("select id from item where uid = %d and `contact-id` = %d and verb ='%s' and deleted = 0 limit 1", + $r = q("select id from item where uid = %d and `contact-id` = %d and verb ='%s' and deleted = 0 and (`parent-uri` = '%s' OR `thr-parent` = '%s') limit 1", intval($datarray['uid']), intval($datarray['contact-id']), - dbesc($datarray['verb']) + dbesc($datarray['verb']), + dbesc($parent_uri), + dbesc($parent_uri) ); if($r && count($r)) continue; @@ -1809,6 +1815,13 @@ function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) } } + if($contact['network'] === NETWORK_OSTATUS || stristr($contact['url'],'twitter.com')) { + if(strlen($datarray['title'])) + unset($datarray['title']); + $datarray['last-child'] = 1; + } + + $r = q("SELECT `uid`, `last-child`, `edited`, `body` FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($item_id), intval($importer['uid']) @@ -1871,24 +1884,23 @@ function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) if(! is_array($contact)) return; - if($contact['network'] === NETWORK_OSTATUS || stristr($contact['url'],'twitter.com')) { - if(strlen($datarray['title'])) - unset($datarray['title']); - $datarray['last-child'] = 1; - } if(($contact['network'] === NETWORK_FEED) || (! strlen($contact['notify']))) { // one way feed - no remote comment ability $datarray['last-child'] = 0; } if($contact['network'] === NETWORK_FEED) - $datarray['private'] = 1; + $datarray['private'] = 2; // This is my contact on another system, but it's really me. // Turn this into a wall post. - if($contact['remote_self']) + if($contact['remote_self']) { $datarray['wall'] = 1; + if($contact['network'] === NETWORK_FEED) { + $datarray['private'] = 0; + } + } $datarray['parent-uri'] = $item_id; $datarray['uid'] = $importer['uid']; @@ -2146,6 +2158,67 @@ function local_delivery($importer,$data) { } if($deleted) { + // check for relayed deletes to our conversation + + $is_reply = false; + $r = q("select * from item where uri = '%s' and uid = %d limit 1", + dbesc($uri), + intval($importer['importer_uid']) + ); + if(count($r)) { + $parent_uri = $r[0]['parent-uri']; + if($r[0]['id'] != $r[0]['parent']) + $is_reply = true; + } + + if($is_reply) { + $community = false; + + if($importer['page-flags'] == PAGE_COMMUNITY || $importer['page-flags'] == PAGE_PRVGROUP ) { + $sql_extra = ''; + $community = true; + logger('local_delivery: possible community delete'); + } + else + $sql_extra = " and contact.self = 1 and item.wall = 1 "; + + // was the top-level post for this reply written by somebody on this site? + // Specifically, the recipient? + + $is_a_remote_delete = false; + + $r = q("select `item`.`id`, `item`.`uri`, `item`.`tag`, `item`.`forum_mode`,`item`.`origin`,`item`.`wall`, + `contact`.`name`, `contact`.`url`, `contact`.`thumb` from `item` + LEFT JOIN `contact` ON `contact`.`id` = `item`.`contact-id` + WHERE `item`.`uri` = '%s' AND (`item`.`parent-uri` = '%s' or `item`.`thr-parent` = '%s') + AND `item`.`uid` = %d + $sql_extra + LIMIT 1", + dbesc($parent_uri), + dbesc($parent_uri), + dbesc($parent_uri), + intval($importer['importer_uid']) + ); + if($r && count($r)) + $is_a_remote_delete = true; + + // Does this have the characteristics of a community or private group comment? + // If it's a reply to a wall post on a community/prvgroup page it's a + // valid community comment. Also forum_mode makes it valid for sure. + // If neither, it's not. + + if($is_a_remote_delete && $community) { + if((! $r[0]['forum_mode']) && (! $r[0]['wall'])) { + $is_a_remote_delete = false; + logger('local_delivery: not a community delete'); + } + } + + if($is_a_remote_delete) { + logger('local_delivery: received remote delete'); + } + } + $r = q("SELECT `item`.*, `contact`.`self` FROM `item` left join contact on `item`.`contact-id` = `contact`.`id` WHERE `uri` = '%s' AND `item`.`uid` = %d AND `contact-id` = %d AND NOT `item`.`file` LIKE '%%[%%' LIMIT 1", dbesc($uri), @@ -2198,7 +2271,8 @@ function local_delivery($importer,$data) { } if($item['uri'] == $item['parent-uri']) { - $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s' + $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', + `body` = '', `title` = '' WHERE `parent-uri` = '%s' AND `uid` = %d", dbesc($when), dbesc(datetime_convert()), @@ -2207,7 +2281,8 @@ function local_delivery($importer,$data) { ); } else { - $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s' + $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', + `body` = '', `title` = '' WHERE `uri` = '%s' AND `uid` = %d LIMIT 1", dbesc($when), dbesc(datetime_convert()), @@ -2233,7 +2308,11 @@ function local_delivery($importer,$data) { ); } } - } + // if this is a relayed delete, propagate it to other recipients + + if($is_a_remote_delete) + proc_run('php',"include/notifier.php","drop",$item['id']); + } } } } @@ -2266,15 +2345,17 @@ function local_delivery($importer,$data) { $is_a_remote_comment = false; + // POSSIBLE CLEANUP --> Why select so many fields when only forum_mode and wall are used? $r = q("select `item`.`id`, `item`.`uri`, `item`.`tag`, `item`.`forum_mode`,`item`.`origin`,`item`.`wall`, `contact`.`name`, `contact`.`url`, `contact`.`thumb` from `item` LEFT JOIN `contact` ON `contact`.`id` = `item`.`contact-id` - WHERE `item`.`uri` = '%s' AND `item`.`parent-uri` = '%s' + WHERE `item`.`uri` = '%s' AND (`item`.`parent-uri` = '%s' or `item`.`thr-parent` = '%s') AND `item`.`uid` = %d $sql_extra LIMIT 1", dbesc($parent_uri), dbesc($parent_uri), + dbesc($parent_uri), intval($importer['importer_uid']) ); if($r && count($r)) @@ -2362,10 +2443,13 @@ function local_delivery($importer,$data) { $datarray['gravity'] = GRAVITY_LIKE; $datarray['last-child'] = 0; // only one like or dislike per person - $r = q("select id from item where uid = %d and `contact-id` = %d and verb ='%s' and deleted = 0 limit 1", + $r = q("select id from item where uid = %d and `contact-id` = %d and verb = '%s' and (`thr-parent` = '%s' or `parent-uri` = '%s') and deleted = 0 limit 1", intval($datarray['uid']), intval($datarray['contact-id']), - dbesc($datarray['verb']) + dbesc($datarray['verb']), + dbesc($datarray['parent-uri']), + dbesc($datarray['parent-uri']) + ); if($r && count($r)) continue; @@ -2533,10 +2617,12 @@ function local_delivery($importer,$data) { $datarray['type'] = 'activity'; $datarray['gravity'] = GRAVITY_LIKE; // only one like or dislike per person - $r = q("select id from item where uid = %d and `contact-id` = %d and verb ='%s' and deleted = 0 limit 1", + $r = q("select id from item where uid = %d and `contact-id` = %d and verb ='%s' and deleted = 0 and (`parent-uri` = '%s' OR `thr-parent` = '%s') limit 1", intval($datarray['uid']), intval($datarray['contact-id']), - dbesc($datarray['verb']) + dbesc($datarray['verb']), + dbesc($parent_uri), + dbesc($parent_uri) ); if($r && count($r)) continue; @@ -2931,8 +3017,10 @@ function atom_entry($item,$type,$author,$owner,$comment = false,$cid = 0) { if(strlen($item['owner-name'])) $o .= atom_author('dfrn:owner',$item['owner-name'],$item['owner-link'],80,80,$item['owner-avatar']); - if(($item['parent'] != $item['id']) || ($item['parent-uri'] !== $item['uri'])) - $o .= '<thr:in-reply-to ref="' . xmlify($item['parent-uri']) . '" type="text/html" href="' . xmlify($a->get_baseurl() . '/display/' . $owner['nickname'] . '/' . $item['parent']) . '" />' . "\r\n"; + if(($item['parent'] != $item['id']) || ($item['parent-uri'] !== $item['uri']) || ($item['thr-parent'])) { + $parent_item = (($item['thr-parent']) ? $item['thr-parent'] : $item['parent-uri']); + $o .= '<thr:in-reply-to ref="' . xmlify($parent_item) . '" type="text/html" href="' . xmlify($a->get_baseurl() . '/display/' . $owner['nickname'] . '/' . $item['parent']) . '" />' . "\r\n"; + } $o .= '<id>' . xmlify($item['uri']) . '</id>' . "\r\n"; $o .= '<title>' . xmlify($item['title']) . '</title>' . "\r\n"; @@ -2953,7 +3041,7 @@ function atom_entry($item,$type,$author,$owner,$comment = false,$cid = 0) { $o .= '<georss:point>' . xmlify($item['coord']) . '</georss:point>' . "\r\n"; if(($item['private']) || strlen($item['allow_cid']) || strlen($item['allow_gid']) || strlen($item['deny_cid']) || strlen($item['deny_gid'])) - $o .= '<dfrn:private>1</dfrn:private>' . "\r\n"; + $o .= '<dfrn:private>' . (($item['private']) ? $item['private'] : 1) . '</dfrn:private>' . "\r\n"; if($item['extid']) $o .= '<dfrn:extid>' . xmlify($item['extid']) . '</dfrn:extid>' . "\r\n"; @@ -3349,40 +3437,8 @@ function drop_item($id,$interactive = true) { ); } - // Add a relayable_retraction signature for Diaspora. Note that we can't add a target_author_signature - // if the comment was deleted by a remote user. That should be ok, because if a remote user is deleting - // the comment, that means we're the home of the post, and Diaspora will only - // check the parent_author_signature of retractions that it doesn't have to relay further - // - // I don't think this function gets called for an "unlike," but I'll check anyway - $signed_text = $item['guid'] . ';' . ( ($item['verb'] === ACTIVITY_LIKE) ? 'Like' : 'Comment'); - - if(local_user() == $item['uid']) { - - $handle = $a->user['nickname'] . '@' . substr($a->get_baseurl(), strpos($a->get_baseurl(),'://') + 3); - $authorsig = base64_encode(rsa_sign($signed_text,$a->user['prvkey'],'sha256')); - } - else { - $r = q("SELECT `nick`, `url` FROM `contact` WHERE `id` = '%d' LIMIT 1", - $item['contact-id'] - ); - if(count($r)) { - // The below handle only works for NETWORK_DFRN. I think that's ok, because this function - // only handles DFRN deletes - $handle_baseurl_start = strpos($r['url'],'://') + 3; - $handle_baseurl_length = strpos($r['url'],'/profile') - $handle_baseurl_start; - $handle = $r['nick'] . '@' . substr($r['url'], $handle_baseurl_start, $handle_baseurl_length); - $authorsig = ''; - } - } - - if(isset($handle)) - q("insert into sign (`retract_iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ", - intval($item['id']), - dbesc($signed_text), - dbesc($authorsig), - dbesc($handle) - ); + // Add a relayable_retraction signature for Diaspora. + store_diaspora_retract_sig($item, $a->user, $a->get_baseurl()); } $drop_id = intval($item['id']); @@ -3469,4 +3525,53 @@ function posted_date_widget($url,$uid,$wall) { '$dates' => $ret )); return $o; -}
\ No newline at end of file +} + + +function store_diaspora_retract_sig($item, $user, $baseurl) { + // Note that we can't add a target_author_signature + // if the comment was deleted by a remote user. That should be ok, because if a remote user is deleting + // the comment, that means we're the home of the post, and Diaspora will only + // check the parent_author_signature of retractions that it doesn't have to relay further + // + // I don't think this function gets called for an "unlike," but I'll check anyway + + $enabled = intval(get_config('system','diaspora_enabled')); + if(! $enabled) { + logger('drop_item: diaspora support disabled, not storing retraction signature', LOGGER_DEBUG); + return; + } + + logger('drop_item: storing diaspora retraction signature'); + + $signed_text = $item['guid'] . ';' . ( ($item['verb'] === ACTIVITY_LIKE) ? 'Like' : 'Comment'); + + if(local_user() == $item['uid']) { + + $handle = $user['nickname'] . '@' . substr($baseurl, strpos($baseurl,'://') + 3); + $authorsig = base64_encode(rsa_sign($signed_text,$user['prvkey'],'sha256')); + } + else { + $r = q("SELECT `nick`, `url` FROM `contact` WHERE `id` = '%d' LIMIT 1", + $item['contact-id'] + ); + if(count($r)) { + // The below handle only works for NETWORK_DFRN. I think that's ok, because this function + // only handles DFRN deletes + $handle_baseurl_start = strpos($r['url'],'://') + 3; + $handle_baseurl_length = strpos($r['url'],'/profile') - $handle_baseurl_start; + $handle = $r['nick'] . '@' . substr($r['url'], $handle_baseurl_start, $handle_baseurl_length); + $authorsig = ''; + } + } + + if(isset($handle)) + q("insert into sign (`retract_iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ", + intval($item['id']), + dbesc($signed_text), + dbesc($authorsig), + dbesc($handle) + ); + + return; +} diff --git a/include/lock.php b/include/lock.php new file mode 100644 index 000000000..5f1ca6323 --- /dev/null +++ b/include/lock.php @@ -0,0 +1,77 @@ +<?php + +// Provide some ability to lock a PHP function so that multiple processes +// can't run the function concurrently +if(! function_exists('lock_function')) { +function lock_function($fn_name, $block = true, $wait_sec = 2, $timeout = 30) { + if( $wait_sec == 0 ) + $wait_sec = 2; // don't let the user pick a value that's likely to crash the system + + $got_lock = false; + $start = time(); + + do { + q("LOCK TABLE locks WRITE"); + $r = q("SELECT locked FROM locks WHERE name = '%s' LIMIT 1", + dbesc($fn_name) + ); + + if((count($r)) && (! $r[0]['locked'])) { + q("UPDATE locks SET locked = 1 WHERE name = '%s' LIMIT 1", + dbesc($fn_name) + ); + $got_lock = true; + } + elseif(! $r) { // the Boolean value for count($r) should be equivalent to the Boolean value of $r + q("INSERT INTO locks ( name, locked ) VALUES ( '%s', 1 )", + dbesc($fn_name) + ); + $got_lock = true; + } + + q("UNLOCK TABLES"); + + if(($block) && (! $got_lock)) + sleep($wait_sec); + + } while(($block) && (! $got_lock) && ((time() - $start) < $timeout)); + + logger('lock_function: function ' . $fn_name . ' with blocking = ' . $block . ' got_lock = ' . $got_lock . ' time = ' . (time() - $start), LOGGER_DEBUG); + + return $got_lock; +}} + + +if(! function_exists('block_on_function_lock')) { +function block_on_function_lock($fn_name, $wait_sec = 2, $timeout = 30) { + if( $wait_sec == 0 ) + $wait_sec = 2; // don't let the user pick a value that's likely to crash the system + + $start = time(); + + do { + $r = q("SELECT locked FROM locks WHERE name = '%s' LIMIT 1", + dbesc($fn_name) + ); + + if(count($r) && $r[0]['locked']) + sleep($wait_sec); + + } while(count($r) && $r[0]['locked'] && ((time() - $start) < $timeout)); + + return; +}} + + +if(! function_exists('unlock_function')) { +function unlock_function($fn_name) { + $r = q("UPDATE locks SET locked = 0 WHERE name = '%s' LIMIT 1", + dbesc($fn_name) + ); + + logger('unlock_function: released lock for function ' . $fn_name, LOGGER_DEBUG); + + return; +}} + +?> diff --git a/include/markdownify/LICENSE_LGPL.txt b/include/markdownify/LICENSE_LGPL.txt new file mode 100644 index 000000000..5ab7695ab --- /dev/null +++ b/include/markdownify/LICENSE_LGPL.txt @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/include/markdownify/TODO b/include/markdownify/TODO new file mode 100644 index 000000000..06ec8508b --- /dev/null +++ b/include/markdownify/TODO @@ -0,0 +1,29 @@ +Markdownify +=========== +* handle non-markdownifiable lists (i.e. `<ul><li id="foobar">asdf</li></ul>`) +* organize methods better (i.e. flushlinebreaks & setlinebreaks close to each other) +* take a look at function names etc. +* is the new (in rev. 93) lastclosedtag property needed? +* word wrapping (some work is done but it's still very buggy) + + +Markdownify Extra +================= + +* handle table alignment with KEEP_HTML=false +* handle tables without headings when KEEP_HTML=false is set +* handle Markdown inside non-markdownable tags + + +Implementation Thoughts +======================= +* non-markdownifiable lists and markdown inside non-markdownable tags as well as the current + table implementation could be rewritten by using a rollback mechanism. + + example: + + <ul><li>asdf</li><li id="foobar">asdf</li></ul> + + we come to `<ul>`, know that this might fail and create a snapshot of our current parser + we keep on parsing and when we reach `<li id="foobar">` we gotta rollback and keep this + list in HTML format. diff --git a/include/markdownify/example.php b/include/markdownify/example.php new file mode 100644 index 000000000..ef86dca83 --- /dev/null +++ b/include/markdownify/example.php @@ -0,0 +1,51 @@ +<?php + error_reporting(E_ALL); + if (!empty($_POST['input'])) { + include 'markdownify_extra.php'; + if (!isset($_POST['leap'])) { + $leap = MDFY_LINKS_EACH_PARAGRAPH; + } else { + $leap = $_POST['leap']; + } + + if (!isset($_POST['keepHTML'])) { + $keephtml = MDFY_KEEPHTML; + } else { + $keephtml = $_POST['keepHTML']; + } + if (!empty($_POST['extra'])) { + $md = new Markdownify_Extra($leap, MDFY_BODYWIDTH, $keephtml); + } else { + $md = new Markdownify($leap, MDFY_BODYWIDTH, $keephtml); + } + if (ini_get('magic_quotes_gpc')) { + $_POST['input'] = stripslashes($_POST['input']); + } + $output = $md->parseString($_POST['input']); + } else { + $_POST['input'] = ''; + } +?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US"> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <title>HTML to Markdown Converter</title> + </head> + <body> + <?php if (empty($_POST['input'])): ?> + <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post"> + <fieldset> + <legend>HTML Input</legend> + <textarea style="width:100%;" cols="85" rows="40" name="input"><?php echo htmlspecialchars($_POST['input'], ENT_NOQUOTES, 'UTF-8'); ?></textarea> + </fieldset> + <label for="extra">Markdownify Extra: <input name="extra" checked="checked" id="extra" type="checkbox" value="1" /></label> + <label for="leap">Links after each block elem: <input name="leap" id="leap" type="checkbox" value="1" /></label> + <label for="keepHTML">keep HTML: <input name="keepHTML" id="keepHTML" type="checkbox" value="1" checked="checked" /></label> + <input type="submit" name="submit" value="submit" /> + </form> + <?php else: ?> + <h1 style="text-align:right;"><a href="<?php echo $_SERVER['PHP_SELF']; ?>">BACK</a></h1> + <pre><?php echo htmlspecialchars($output, ENT_NOQUOTES, 'UTF-8'); ?></pre> + <?php endif; ?> + </body> +</html>
\ No newline at end of file diff --git a/include/markdownify/markdownify.php b/include/markdownify/markdownify.php new file mode 100644 index 000000000..43730cb77 --- /dev/null +++ b/include/markdownify/markdownify.php @@ -0,0 +1,1184 @@ +<?php +/** + * Markdownify converts HTML Markup to [Markdown][1] (by [John Gruber][2]. It + * also supports [Markdown Extra][3] by [Michel Fortin][4] via Markdownify_Extra. + * + * It all started as `html2text.php` - a port of [Aaron Swartz'][5] [`html2text.py`][6] - but + * got a long way since. This is far more than a mere port now! + * Starting with version 2.0.0 this is a complete rewrite and cannot be + * compared to Aaron Swatz' `html2text.py` anylonger. I'm now using a HTML parser + * (see `parsehtml.php` which I also wrote) which makes most of the evil + * RegEx magic go away and additionally it gives a much cleaner class + * structure. Also notably is the fact that I now try to prevent regressions by + * utilizing testcases of Michel Fortin's [MDTest][7]. + * + * [1]: http://daringfireball.com/projects/markdown + * [2]: http://daringfireball.com/ + * [3]: http://www.michelf.com/projects/php-markdown/extra/ + * [4]: http://www.michelf.com/ + * [5]: http://www.aaronsw.com/ + * [6]: http://www.aaronsw.com/2002/html2text/ + * [7]: http://article.gmane.org/gmane.text.markdown.general/2540 + * + * @version 2.0.0 alpha + * @author Milian Wolff (<mail@milianw.de>, <http://milianw.de>) + * @license LGPL, see LICENSE_LGPL.txt and the summary below + * @copyright (C) 2007 Milian Wolff + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * HTML Parser, see http://sf.net/projects/parseHTML + */ +require_once dirname(__FILE__).'/parsehtml/parsehtml.php'; + +/** + * default configuration + */ +define('MDFY_LINKS_EACH_PARAGRAPH', false); +define('MDFY_BODYWIDTH', false); +define('MDFY_KEEPHTML', true); + +/** + * HTML to Markdown converter class + */ +class Markdownify { + /** + * html parser object + * + * @var parseHTML + */ + var $parser; + /** + * markdown output + * + * @var string + */ + var $output; + /** + * stack with tags which where not converted to html + * + * @var array<string> + */ + var $notConverted = array(); + /** + * skip conversion to markdown + * + * @var bool + */ + var $skipConversion = false; + /* options */ + /** + * keep html tags which cannot be converted to markdown + * + * @var bool + */ + var $keepHTML = false; + /** + * wrap output, set to 0 to skip wrapping + * + * @var int + */ + var $bodyWidth = 0; + /** + * minimum body width + * + * @var int + */ + var $minBodyWidth = 25; + /** + * display links after each paragraph + * + * @var bool + */ + var $linksAfterEachParagraph = false; + /** + * constructor, set options, setup parser + * + * @param bool $linksAfterEachParagraph wether or not to flush stacked links after each paragraph + * defaults to false + * @param int $bodyWidth wether or not to wrap the output to the given width + * defaults to false + * @param bool $keepHTML wether to keep non markdownable HTML or to discard it + * defaults to true (HTML will be kept) + * @return void + */ + function Markdownify($linksAfterEachParagraph = MDFY_LINKS_EACH_PARAGRAPH, $bodyWidth = MDFY_BODYWIDTH, $keepHTML = MDFY_KEEPHTML) { + $this->linksAfterEachParagraph = $linksAfterEachParagraph; + $this->keepHTML = $keepHTML; + + if ($bodyWidth > $this->minBodyWidth) { + $this->bodyWidth = intval($bodyWidth); + } else { + $this->bodyWidth = false; + } + + $this->parser = new parseHTML; + $this->parser->noTagsInCode = true; + + # we don't have to do this every time + $search = array(); + $replace = array(); + foreach ($this->escapeInText as $s => $r) { + array_push($search, '#(?<!\\\)'.$s.'#U'); + array_push($replace, $r); + } + $this->escapeInText = array( + 'search' => $search, + 'replace' => $replace + ); + } + /** + * parse a HTML string + * + * @param string $html + * @return string markdown formatted + */ + function parseString($html) { + $this->parser->html = $html; + $this->parse(); + return $this->output; + } + /** + * tags with elements which can be handled by markdown + * + * @var array<string> + */ + var $isMarkdownable = array( + 'p' => array(), + 'ul' => array(), + 'ol' => array(), + 'li' => array(), + 'br' => array(), + 'blockquote' => array(), + 'code' => array(), + 'pre' => array(), + 'a' => array( + 'href' => 'required', + 'title' => 'optional', + ), + 'strong' => array(), + 'b' => array(), + 'em' => array(), + 'i' => array(), + 'img' => array( + 'src' => 'required', + 'alt' => 'optional', + 'title' => 'optional', + ), + 'h1' => array(), + 'h2' => array(), + 'h3' => array(), + 'h4' => array(), + 'h5' => array(), + 'h6' => array(), + 'hr' => array(), + ); + /** + * html tags to be ignored (contents will be parsed) + * + * @var array<string> + */ + var $ignore = array( + 'html', + 'body', + ); + /** + * html tags to be dropped (contents will not be parsed!) + * + * @var array<string> + */ + var $drop = array( + 'script', + 'head', + 'style', + 'form', + 'area', + 'object', + 'param', + 'iframe', + ); + /** + * Markdown indents which could be wrapped + * @note: use strings in regex format + * + * @var array<string> + */ + var $wrappableIndents = array( + '\* ', # ul + '\d. ', # ol + '\d\d. ', # ol + '> ', # blockquote + '', # p + ); + /** + * list of chars which have to be escaped in normal text + * @note: use strings in regex format + * + * @var array + * + * TODO: what's with block chars / sequences at the beginning of a block? + */ + var $escapeInText = array( + '([-*_])([ ]{0,2}\1){2,}' => '\\\\$0|', # hr + '\*\*([^*\s]+)\*\*' => '\*\*$1\*\*', # strong + '\*([^*\s]+)\*' => '\*$1\*', # em + '__(?! |_)(.+)(?!<_| )__' => '\_\_$1\_\_', # em + '_(?! |_)(.+)(?!<_| )_' => '\_$1\_', # em + '`(.+)`' => '\`$1\`', # code + '\[(.+)\](\s*\()' => '\[$1\]$2', # links: [text] (url) => [text\] (url) + '\[(.+)\](\s*)\[(.*)\]' => '\[$1\]$2\[$3\]', # links: [text][id] => [text\][id\] + ); + /** + * wether last processed node was a block tag or not + * + * @var bool + */ + var $lastWasBlockTag = false; + /** + * name of last closed tag + * + * @var string + */ + var $lastClosedTag = ''; + /** + * iterate through the nodes and decide what we + * shall do with the current node + * + * @param void + * @return void + */ + function parse() { + $this->output = ''; + # drop tags + $this->parser->html = preg_replace('#<('.implode('|', $this->drop).')[^>]*>.*</\\1>#sU', '', $this->parser->html); + while ($this->parser->nextNode()) { + switch ($this->parser->nodeType) { + case 'doctype': + break; + case 'pi': + case 'comment': + if ($this->keepHTML) { + $this->flushLinebreaks(); + $this->out($this->parser->node); + $this->setLineBreaks(2); + } + # else drop + break; + case 'text': + $this->handleText(); + break; + case 'tag': + if (in_array($this->parser->tagName, $this->ignore)) { + break; + } + if ($this->parser->isStartTag) { + $this->flushLinebreaks(); + } + if ($this->skipConversion) { + $this->isMarkdownable(); # update notConverted + $this->handleTagToText(); + continue; + } + if (!$this->parser->keepWhitespace && $this->parser->isBlockElement && $this->parser->isStartTag) { + $this->parser->html = ltrim($this->parser->html); + } + if ($this->isMarkdownable()) { + if ($this->parser->isBlockElement && $this->parser->isStartTag && !$this->lastWasBlockTag && !empty($this->output)) { + if (!empty($this->buffer)) { + $str =& $this->buffer[count($this->buffer) -1]; + } else { + $str =& $this->output; + } + if (substr($str, -strlen($this->indent)-1) != "\n".$this->indent) { + $str .= "\n".$this->indent; + } + } + $func = 'handleTag_'.$this->parser->tagName; + $this->$func(); + if ($this->linksAfterEachParagraph && $this->parser->isBlockElement && !$this->parser->isStartTag && empty($this->parser->openTags)) { + $this->flushStacked(); + } + if (!$this->parser->isStartTag) { + $this->lastClosedTag = $this->parser->tagName; + } + } else { + $this->handleTagToText(); + $this->lastClosedTag = ''; + } + break; + default: + trigger_error('invalid node type', E_USER_ERROR); + break; + } + $this->lastWasBlockTag = $this->parser->nodeType == 'tag' && $this->parser->isStartTag && $this->parser->isBlockElement; + } + if (!empty($this->buffer)) { + trigger_error('buffer was not flushed, this is a bug. please report!', E_USER_WARNING); + while (!empty($this->buffer)) { + $this->out($this->unbuffer()); + } + } + ### cleanup + $this->output = rtrim(str_replace('&', '&', str_replace('<', '<', str_replace('>', '>', $this->output)))); + # end parsing, flush stacked tags + $this->flushStacked(); + $this->stack = array(); + } + /** + * check if current tag can be converted to Markdown + * + * @param void + * @return bool + */ + function isMarkdownable() { + if (!isset($this->isMarkdownable[$this->parser->tagName])) { + # simply not markdownable + return false; + } + if ($this->parser->isStartTag) { + $return = true; + if ($this->keepHTML) { + $diff = array_diff(array_keys($this->parser->tagAttributes), array_keys($this->isMarkdownable[$this->parser->tagName])); + if (!empty($diff)) { + # non markdownable attributes given + $return = false; + } + } + if ($return) { + foreach ($this->isMarkdownable[$this->parser->tagName] as $attr => $type) { + if ($type == 'required' && !isset($this->parser->tagAttributes[$attr])) { + # required markdown attribute not given + $return = false; + break; + } + } + } + if (!$return) { + array_push($this->notConverted, $this->parser->tagName.'::'.implode('/', $this->parser->openTags)); + } + return $return; + } else { + if (!empty($this->notConverted) && end($this->notConverted) === $this->parser->tagName.'::'.implode('/', $this->parser->openTags)) { + array_pop($this->notConverted); + return false; + } + return true; + } + } + /** + * output all stacked tags + * + * @param void + * @return void + */ + function flushStacked() { + # links + foreach ($this->stack as $tag => $a) { + if (!empty($a)) { + call_user_func(array(&$this, 'flushStacked_'.$tag)); + } + } + } + /** + * output link references (e.g. [1]: http://example.com "title"); + * + * @param void + * @return void + */ + function flushStacked_a() { + $out = false; + foreach ($this->stack['a'] as $k => $tag) { + if (!isset($tag['unstacked'])) { + if (!$out) { + $out = true; + $this->out("\n\n", true); + } else { + $this->out("\n", true); + } + $this->out(' ['.$tag['linkID'].']: '.$tag['href'].(isset($tag['title']) ? ' "'.$tag['title'].'"' : ''), true); + $tag['unstacked'] = true; + $this->stack['a'][$k] = $tag; + } + } + } + /** + * flush enqued linebreaks + * + * @param void + * @return void + */ + function flushLinebreaks() { + if ($this->lineBreaks && !empty($this->output)) { + $this->out(str_repeat("\n".$this->indent, $this->lineBreaks), true); + } + $this->lineBreaks = 0; + } + /** + * handle non Markdownable tags + * + * @param void + * @return void + */ + function handleTagToText() { + if (!$this->keepHTML) { + if (!$this->parser->isStartTag && $this->parser->isBlockElement) { + $this->setLineBreaks(2); + } + } else { + # dont convert to markdown inside this tag + /** TODO: markdown extra **/ + if (!$this->parser->isEmptyTag) { + if ($this->parser->isStartTag) { + if (!$this->skipConversion) { + $this->skipConversion = $this->parser->tagName.'::'.implode('/', $this->parser->openTags); + } + } else { + if ($this->skipConversion == $this->parser->tagName.'::'.implode('/', $this->parser->openTags)) { + $this->skipConversion = false; + } + } + } + + if ($this->parser->isBlockElement) { + if ($this->parser->isStartTag) { + if (in_array($this->parent(), array('ins', 'del'))) { + # looks like ins or del are block elements now + $this->out("\n", true); + $this->indent(' '); + } + if ($this->parser->tagName != 'pre') { + $this->out($this->parser->node."\n".$this->indent); + if (!$this->parser->isEmptyTag) { + $this->indent(' '); + } else { + $this->setLineBreaks(1); + } + $this->parser->html = ltrim($this->parser->html); + } else { + # don't indent inside <pre> tags + $this->out($this->parser->node); + static $indent; + $indent = $this->indent; + $this->indent = ''; + } + } else { + if (!$this->parser->keepWhitespace) { + $this->output = rtrim($this->output); + } + if ($this->parser->tagName != 'pre') { + $this->indent(' '); + $this->out("\n".$this->indent.$this->parser->node); + } else { + # reset indentation + $this->out($this->parser->node); + static $indent; + $this->indent = $indent; + } + + if (in_array($this->parent(), array('ins', 'del'))) { + # ins or del was block element + $this->out("\n"); + $this->indent(' '); + } + if ($this->parser->tagName == 'li') { + $this->setLineBreaks(1); + } else { + $this->setLineBreaks(2); + } + } + } else { + $this->out($this->parser->node); + } + if (in_array($this->parser->tagName, array('code', 'pre'))) { + if ($this->parser->isStartTag) { + $this->buffer(); + } else { + # add stuff so cleanup just reverses this + $this->out(str_replace('<', '&lt;', str_replace('>', '&gt;', $this->unbuffer()))); + } + } + } + } + /** + * handle plain text + * + * @param void + * @return void + */ + function handleText() { + if ($this->hasParent('pre') && strpos($this->parser->node, "\n") !== false) { + $this->parser->node = str_replace("\n", "\n".$this->indent, $this->parser->node); + } + if (!$this->hasParent('code') && !$this->hasParent('pre')) { + # entity decode + $this->parser->node = $this->decode($this->parser->node); + if (!$this->skipConversion) { + # escape some chars in normal Text + $this->parser->node = preg_replace($this->escapeInText['search'], $this->escapeInText['replace'], $this->parser->node); + } + } else { + $this->parser->node = str_replace(array('"', '&apos'), array('"', '\''), $this->parser->node); + } + $this->out($this->parser->node); + $this->lastClosedTag = ''; + } + /** + * handle <em> and <i> tags + * + * @param void + * @return void + */ + function handleTag_em() { + $this->out('*', true); + } + function handleTag_i() { + $this->handleTag_em(); + } + /** + * handle <strong> and <b> tags + * + * @param void + * @return void + */ + function handleTag_strong() { + $this->out('**', true); + } + function handleTag_b() { + $this->handleTag_strong(); + } + /** + * handle <h1> tags + * + * @param void + * @return void + */ + function handleTag_h1() { + $this->handleHeader(1); + } + /** + * handle <h2> tags + * + * @param void + * @return void + */ + function handleTag_h2() { + $this->handleHeader(2); + } + /** + * handle <h3> tags + * + * @param void + * @return void + */ + function handleTag_h3() { + $this->handleHeader(3); + } + /** + * handle <h4> tags + * + * @param void + * @return void + */ + function handleTag_h4() { + $this->handleHeader(4); + } + /** + * handle <h5> tags + * + * @param void + * @return void + */ + function handleTag_h5() { + $this->handleHeader(5); + } + /** + * handle <h6> tags + * + * @param void + * @return void + */ + function handleTag_h6() { + $this->handleHeader(6); + } + /** + * number of line breaks before next inline output + */ + var $lineBreaks = 0; + /** + * handle header tags (<h1> - <h6>) + * + * @param int $level 1-6 + * @return void + */ + function handleHeader($level) { + if ($this->parser->isStartTag) { + $this->out(str_repeat('#', $level).' ', true); + } else { + $this->setLineBreaks(2); + } + } + /** + * handle <p> tags + * + * @param void + * @return void + */ + function handleTag_p() { + if (!$this->parser->isStartTag) { + $this->setLineBreaks(2); + } + } + /** + * handle <a> tags + * + * @param void + * @return void + */ + function handleTag_a() { + if ($this->parser->isStartTag) { + $this->buffer(); + if (isset($this->parser->tagAttributes['title'])) { + $this->parser->tagAttributes['title'] = $this->decode($this->parser->tagAttributes['title']); + } else { + $this->parser->tagAttributes['title'] = null; + } + $this->parser->tagAttributes['href'] = $this->decode(trim($this->parser->tagAttributes['href'])); + $this->stack(); + } else { + $tag = $this->unstack(); + $buffer = $this->unbuffer(); + + if (empty($tag['href']) && empty($tag['title'])) { + # empty links... testcase mania, who would possibly do anything like that?! + $this->out('['.$buffer.']()', true); + return; + } + + if ($buffer == $tag['href'] && empty($tag['title'])) { + # <http://example.com> + $this->out('<'.$buffer.'>', true); + return; + } + + $bufferDecoded = $this->decode(trim($buffer)); + if (substr($tag['href'], 0, 7) == 'mailto:' && 'mailto:'.$bufferDecoded == $tag['href']) { + if (is_null($tag['title'])) { + # <mail@example.com> + $this->out('<'.$bufferDecoded.'>', true); + return; + } + # [mail@example.com][1] + # ... + # [1]: mailto:mail@example.com Title + $tag['href'] = 'mailto:'.$bufferDecoded; + } + # [This link][id] + foreach ($this->stack['a'] as $tag2) { + if ($tag2['href'] == $tag['href'] && $tag2['title'] === $tag['title']) { + $tag['linkID'] = $tag2['linkID']; + break; + } + } + if (!isset($tag['linkID'])) { + $tag['linkID'] = count($this->stack['a']) + 1; + array_push($this->stack['a'], $tag); + } + + $this->out('['.$buffer.']['.$tag['linkID'].']', true); + } + } + /** + * handle <img /> tags + * + * @param void + * @return void + */ + function handleTag_img() { + if (!$this->parser->isStartTag) { + return; # just to be sure this is really an empty tag... + } + + if (isset($this->parser->tagAttributes['title'])) { + $this->parser->tagAttributes['title'] = $this->decode($this->parser->tagAttributes['title']); + } else { + $this->parser->tagAttributes['title'] = null; + } + if (isset($this->parser->tagAttributes['alt'])) { + $this->parser->tagAttributes['alt'] = $this->decode($this->parser->tagAttributes['alt']); + } else { + $this->parser->tagAttributes['alt'] = null; + } + + if (empty($this->parser->tagAttributes['src'])) { + # support for "empty" images... dunno if this is really needed + # but there are some testcases which do that... + if (!empty($this->parser->tagAttributes['title'])) { + $this->parser->tagAttributes['title'] = ' '.$this->parser->tagAttributes['title'].' '; + } + $this->out('!['.$this->parser->tagAttributes['alt'].']('.$this->parser->tagAttributes['title'].')', true); + return; + } else { + $this->parser->tagAttributes['src'] = $this->decode($this->parser->tagAttributes['src']); + } + + # [This link][id] + $link_id = false; + if (!empty($this->stack['a'])) { + foreach ($this->stack['a'] as $tag) { + if ($tag['href'] == $this->parser->tagAttributes['src'] + && $tag['title'] === $this->parser->tagAttributes['title']) { + $link_id = $tag['linkID']; + break; + } + } + } else { + $this->stack['a'] = array(); + } + if (!$link_id) { + $link_id = count($this->stack['a']) + 1; + $tag = array( + 'href' => $this->parser->tagAttributes['src'], + 'linkID' => $link_id, + 'title' => $this->parser->tagAttributes['title'] + ); + array_push($this->stack['a'], $tag); + } + + $this->out('!['.$this->parser->tagAttributes['alt'].']['.$link_id.']', true); + } + /** + * handle <code> tags + * + * @param void + * @return void + */ + function handleTag_code() { + if ($this->hasParent('pre')) { + # ignore code blocks inside <pre> + return; + } + if ($this->parser->isStartTag) { + $this->buffer(); + } else { + $buffer = $this->unbuffer(); + # use as many backticks as needed + preg_match_all('#`+#', $buffer, $matches); + if (!empty($matches[0])) { + rsort($matches[0]); + + $ticks = '`'; + while (true) { + if (!in_array($ticks, $matches[0])) { + break; + } + $ticks .= '`'; + } + } else { + $ticks = '`'; + } + if ($buffer[0] == '`' || substr($buffer, -1) == '`') { + $buffer = ' '.$buffer.' '; + } + $this->out($ticks.$buffer.$ticks, true); + } + } + /** + * handle <pre> tags + * + * @param void + * @return void + */ + function handleTag_pre() { + if ($this->keepHTML && $this->parser->isStartTag) { + # check if a simple <code> follows + if (!preg_match('#^\s*<code\s*>#Us', $this->parser->html)) { + # this is no standard markdown code block + $this->handleTagToText(); + return; + } + } + $this->indent(' '); + if (!$this->parser->isStartTag) { + $this->setLineBreaks(2); + } else { + $this->parser->html = ltrim($this->parser->html); + } + } + /** + * handle <blockquote> tags + * + * @param void + * @return void + */ + function handleTag_blockquote() { + $this->indent('> '); + } + /** + * handle <ul> tags + * + * @param void + * @return void + */ + function handleTag_ul() { + if ($this->parser->isStartTag) { + $this->stack(); + if (!$this->keepHTML && $this->lastClosedTag == $this->parser->tagName) { + $this->out("\n".$this->indent.'<!-- -->'."\n".$this->indent."\n".$this->indent); + } + } else { + $this->unstack(); + if ($this->parent() != 'li' || preg_match('#^\s*(</li\s*>\s*<li\s*>\s*)?<(p|blockquote)\s*>#sU', $this->parser->html)) { + # dont make Markdown add unneeded paragraphs + $this->setLineBreaks(2); + } + } + } + /** + * handle <ul> tags + * + * @param void + * @return void + */ + function handleTag_ol() { + # same as above + $this->parser->tagAttributes['num'] = 0; + $this->handleTag_ul(); + } + /** + * handle <li> tags + * + * @param void + * @return void + */ + function handleTag_li() { + if ($this->parent() == 'ol') { + $parent =& $this->getStacked('ol'); + if ($this->parser->isStartTag) { + $parent['num']++; + $this->out($parent['num'].'.'.str_repeat(' ', 3 - strlen($parent['num'])), true); + } + $this->indent(' ', false); + } else { + if ($this->parser->isStartTag) { + $this->out('* ', true); + } + $this->indent(' ', false); + } + if (!$this->parser->isStartTag) { + $this->setLineBreaks(1); + } + } + /** + * handle <hr /> tags + * + * @param void + * @return void + */ + function handleTag_hr() { + if (!$this->parser->isStartTag) { + return; # just to be sure this really is an empty tag + } + $this->out('* * *', true); + $this->setLineBreaks(2); + } + /** + * handle <br /> tags + * + * @param void + * @return void + */ + function handleTag_br() { + $this->out(" \n".$this->indent, true); + $this->parser->html = ltrim($this->parser->html); + } + /** + * node stack, e.g. for <a> and <abbr> tags + * + * @var array<array> + */ + var $stack = array(); + /** + * add current node to the stack + * this only stores the attributes + * + * @param void + * @return void + */ + function stack() { + if (!isset($this->stack[$this->parser->tagName])) { + $this->stack[$this->parser->tagName] = array(); + } + array_push($this->stack[$this->parser->tagName], $this->parser->tagAttributes); + } + /** + * remove current tag from stack + * + * @param void + * @return array + */ + function unstack() { + if (!isset($this->stack[$this->parser->tagName]) || !is_array($this->stack[$this->parser->tagName])) { + trigger_error('Trying to unstack from empty stack. This must not happen.', E_USER_ERROR); + } + return array_pop($this->stack[$this->parser->tagName]); + } + /** + * get last stacked element of type $tagName + * + * @param string $tagName + * @return array + */ + function & getStacked($tagName) { + // no end() so it can be referenced + return $this->stack[$tagName][count($this->stack[$tagName])-1]; + } + /** + * set number of line breaks before next start tag + * + * @param int $number + * @return void + */ + function setLineBreaks($number) { + if ($this->lineBreaks < $number) { + $this->lineBreaks = $number; + } + } + /** + * stores current buffers + * + * @var array<string> + */ + var $buffer = array(); + /** + * buffer next parser output until unbuffer() is called + * + * @param void + * @return void + */ + function buffer() { + array_push($this->buffer, ''); + } + /** + * end current buffer and return buffered output + * + * @param void + * @return string + */ + function unbuffer() { + return array_pop($this->buffer); + } + /** + * append string to the correct var, either + * directly to $this->output or to the current + * buffers + * + * @param string $put + * @return void + */ + function out($put, $nowrap = false) { + if (empty($put)) { + return; + } + if (!empty($this->buffer)) { + $this->buffer[count($this->buffer) - 1] .= $put; + } else { + if ($this->bodyWidth && !$this->parser->keepWhitespace) { # wrap lines + // get last line + $pos = strrpos($this->output, "\n"); + if ($pos === false) { + $line = $this->output; + } else { + $line = substr($this->output, $pos); + } + + if ($nowrap) { + if ($put[0] != "\n" && $this->strlen($line) + $this->strlen($put) > $this->bodyWidth) { + $this->output .= "\n".$this->indent.$put; + } else { + $this->output .= $put; + } + return; + } else { + $put .= "\n"; # make sure we get all lines in the while below + $lineLen = $this->strlen($line); + while ($pos = strpos($put, "\n")) { + $putLine = substr($put, 0, $pos+1); + $put = substr($put, $pos+1); + $putLen = $this->strlen($putLine); + if ($lineLen + $putLen < $this->bodyWidth) { + $this->output .= $putLine; + $lineLen = $putLen; + } else { + $split = preg_split('#^(.{0,'.($this->bodyWidth - $lineLen).'})\b#', $putLine, 2, PREG_SPLIT_OFFSET_CAPTURE | PREG_SPLIT_DELIM_CAPTURE); + $this->output .= rtrim($split[1][0])."\n".$this->indent.$this->wordwrap(ltrim($split[2][0]), $this->bodyWidth, "\n".$this->indent, false); + } + } + $this->output = substr($this->output, 0, -1); + return; + } + } else { + $this->output .= $put; + } + } + } + /** + * current indentation + * + * @var string + */ + var $indent = ''; + /** + * indent next output (start tag) or unindent (end tag) + * + * @param string $str indentation + * @param bool $output add indendation to output + * @return void + */ + function indent($str, $output = true) { + if ($this->parser->isStartTag) { + $this->indent .= $str; + if ($output) { + $this->out($str, true); + } + } else { + $this->indent = substr($this->indent, 0, -strlen($str)); + } + } + /** + * decode email addresses + * + * @author derernst@gmx.ch <http://www.php.net/manual/en/function.html-entity-decode.php#68536> + * @author Milian Wolff <http://milianw.de> + */ + function decode($text, $quote_style = ENT_QUOTES) { + if (version_compare(PHP_VERSION, '5', '>=')) { + # UTF-8 is only supported in PHP 5.x.x and above + $text = html_entity_decode($text, $quote_style, 'UTF-8'); + } else { + if (function_exists('html_entity_decode')) { + $text = html_entity_decode($text, $quote_style, 'ISO-8859-1'); + } else { + static $trans_tbl; + if (!isset($trans_tbl)) { + $trans_tbl = array_flip(get_html_translation_table(HTML_ENTITIES, $quote_style)); + } + $text = strtr($text, $trans_tbl); + } + $text = preg_replace_callback('~&#x([0-9a-f]+);~i', array(&$this, '_decode_hex'), $text); + $text = preg_replace_callback('~&#(\d{2,5});~', array(&$this, '_decode_numeric'), $text); + } + return $text; + } + /** + * callback for decode() which converts a hexadecimal entity to UTF-8 + * + * @param array $matches + * @return string UTF-8 encoded + */ + function _decode_hex($matches) { + return $this->unichr(hexdec($matches[1])); + } + /** + * callback for decode() which converts a numerical entity to UTF-8 + * + * @param array $matches + * @return string UTF-8 encoded + */ + function _decode_numeric($matches) { + return $this->unichr($matches[1]); + } + /** + * UTF-8 chr() which supports numeric entities + * + * @author grey - greywyvern - com <http://www.php.net/manual/en/function.chr.php#55978> + * @param array $matches + * @return string UTF-8 encoded + */ + function unichr($dec) { + if ($dec < 128) { + $utf = chr($dec); + } else if ($dec < 2048) { + $utf = chr(192 + (($dec - ($dec % 64)) / 64)); + $utf .= chr(128 + ($dec % 64)); + } else { + $utf = chr(224 + (($dec - ($dec % 4096)) / 4096)); + $utf .= chr(128 + ((($dec % 4096) - ($dec % 64)) / 64)); + $utf .= chr(128 + ($dec % 64)); + } + return $utf; + } + /** + * UTF-8 strlen() + * + * @param string $str + * @return int + * + * @author dtorop 932 at hotmail dot com <http://www.php.net/manual/en/function.strlen.php#37975> + * @author Milian Wolff <http://milianw.de> + */ + function strlen($str) { + if (function_exists('mb_strlen')) { + return mb_strlen($str, 'UTF-8'); + } else { + return preg_match_all('/[\x00-\x7F\xC0-\xFD]/', $str, $var_empty); + } + } + /** + * wordwrap for utf8 encoded strings + * + * @param string $str + * @param integer $len + * @param string $what + * @return string + */ + function wordwrap($str, $width, $break, $cut = false){ + if (!$cut) { + $regexp = '#^(?:[\x00-\x7F]|[\xC0-\xFF][\x80-\xBF]+){1,'.$width.'}\b#'; + } else { + $regexp = '#^(?:[\x00-\x7F]|[\xC0-\xFF][\x80-\xBF]+){'.$width.'}#'; + } + $return = ''; + while (preg_match($regexp, $str, $matches)) { + $string = $matches[0]; + $str = ltrim(substr($str, strlen($string))); + if (!$cut && isset($str[0]) && in_array($str[0], array('.', '!', ';', ':', '?', ','))) { + $string .= $str[0]; + $str = ltrim(substr($str, 1)); + } + $return .= $string.$break; + } + return $return.ltrim($str); + } + /** + * check if current node has a $tagName as parent (somewhere, not only the direct parent) + * + * @param string $tagName + * @return bool + */ + function hasParent($tagName) { + return in_array($tagName, $this->parser->openTags); + } + /** + * get tagName of direct parent tag + * + * @param void + * @return string $tagName + */ + function parent() { + return end($this->parser->openTags); + } +}
\ No newline at end of file diff --git a/include/markdownify/markdownify_cli.php b/include/markdownify/markdownify_cli.php new file mode 100755 index 000000000..b3fffbd5c --- /dev/null +++ b/include/markdownify/markdownify_cli.php @@ -0,0 +1,33 @@ +#!/usr/bin/php +<?php +require dirname(__FILE__) .'/markdownify_extra.php'; + +function param($name, $default = false) { + if (!in_array('--'.$name, $_SERVER['argv'])) + return $default; + reset($_SERVER['argv']); + while (each($_SERVER['argv'])) { + if (current($_SERVER['argv']) == '--'.$name) + break; + } + $value = next($_SERVER['argv']); + if ($value === false || substr($value, 0, 2) == '--') + return true; + else + return $value; +} + + +$input = stream_get_contents(STDIN); + +$linksAfterEachParagraph = param('links'); +$bodyWidth = param('width'); +$keepHTML = param('html', true); + +if (param('no_extra')) { + $parser = new Markdownify($linksAfterEachParagraph, $bodyWidth, $keepHTML); +} else { + $parser = new Markdownify_Extra($linksAfterEachParagraph, $bodyWidth, $keepHTML); +} + +echo $parser->parseString($input) ."\n";
\ No newline at end of file diff --git a/include/markdownify/markdownify_extra.php b/include/markdownify/markdownify_extra.php new file mode 100644 index 000000000..e978a1c8a --- /dev/null +++ b/include/markdownify/markdownify_extra.php @@ -0,0 +1,489 @@ +<?php +/** + * Class to convert HTML to Markdown with PHP Markdown Extra syntax support. + * + * @version 1.0.0 alpha + * @author Milian Wolff (<mail@milianw.de>, <http://milianw.de>) + * @license LGPL, see LICENSE_LGPL.txt and the summary below + * @copyright (C) 2007 Milian Wolff + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * standard Markdownify class + */ +require_once dirname(__FILE__).'/markdownify.php'; + +class Markdownify_Extra extends Markdownify { + /** + * table data, including rows with content and the maximum width of each col + * + * @var array + */ + var $table = array(); + /** + * current col + * + * @var int + */ + var $col = -1; + /** + * current row + * + * @var int + */ + var $row = 0; + /** + * constructor, see Markdownify::Markdownify() for more information + */ + function Markdownify_Extra($linksAfterEachParagraph = MDFY_LINKS_EACH_PARAGRAPH, $bodyWidth = MDFY_BODYWIDTH, $keepHTML = MDFY_KEEPHTML) { + parent::Markdownify($linksAfterEachParagraph, $bodyWidth, $keepHTML); + + ### new markdownable tags & attributes + # header ids: # foo {bar} + $this->isMarkdownable['h1']['id'] = 'optional'; + $this->isMarkdownable['h2']['id'] = 'optional'; + $this->isMarkdownable['h3']['id'] = 'optional'; + $this->isMarkdownable['h4']['id'] = 'optional'; + $this->isMarkdownable['h5']['id'] = 'optional'; + $this->isMarkdownable['h6']['id'] = 'optional'; + # tables + $this->isMarkdownable['table'] = array(); + $this->isMarkdownable['th'] = array( + 'align' => 'optional', + ); + $this->isMarkdownable['td'] = array( + 'align' => 'optional', + ); + $this->isMarkdownable['tr'] = array(); + array_push($this->ignore, 'thead'); + array_push($this->ignore, 'tbody'); + array_push($this->ignore, 'tfoot'); + # definition lists + $this->isMarkdownable['dl'] = array(); + $this->isMarkdownable['dd'] = array(); + $this->isMarkdownable['dt'] = array(); + # footnotes + $this->isMarkdownable['fnref'] = array( + 'target' => 'required', + ); + $this->isMarkdownable['footnotes'] = array(); + $this->isMarkdownable['fn'] = array( + 'name' => 'required', + ); + $this->parser->blockElements['fnref'] = false; + $this->parser->blockElements['fn'] = true; + $this->parser->blockElements['footnotes'] = true; + # abbr + $this->isMarkdownable['abbr'] = array( + 'title' => 'required', + ); + # build RegEx lookahead to decide wether table can pe parsed or not + $inlineTags = array_keys($this->parser->blockElements, false); + $colContents = '(?:[^<]|<(?:'.implode('|', $inlineTags).'|[^a-z]))+'; + $this->tableLookaheadHeader = '{ + ^\s*(?:<thead\s*>)?\s* # open optional thead + <tr\s*>\s*(?: # start required row with headers + <th(?:\s+align=("|\')(?:left|center|right)\1)?\s*> # header with optional align + \s*'.$colContents.'\s* # contents + </th>\s* # close header + )+</tr> # close row with headers + \s*(?:</thead>)? # close optional thead + }sxi'; + $this->tdSubstitute = '\s*'.$colContents.'\s* # contents + </td>\s*'; + $this->tableLookaheadBody = '{ + \s*(?:<tbody\s*>)?\s* # open optional tbody + (?:<tr\s*>\s* # start row + %s # cols to be substituted + </tr>)+ # close row + \s*(?:</tbody>)? # close optional tbody + \s*</table> # close table + }sxi'; + } + /** + * handle header tags (<h1> - <h6>) + * + * @param int $level 1-6 + * @return void + */ + function handleHeader($level) { + static $id = null; + if ($this->parser->isStartTag) { + if (isset($this->parser->tagAttributes['id'])) { + $id = $this->parser->tagAttributes['id']; + } + } else { + if (!is_null($id)) { + $this->out(' {#'.$id.'}'); + $id = null; + } + } + parent::handleHeader($level); + } + /** + * handle <abbr> tags + * + * @param void + * @return void + */ + function handleTag_abbr() { + if ($this->parser->isStartTag) { + $this->stack(); + $this->buffer(); + } else { + $tag = $this->unstack(); + $tag['text'] = $this->unbuffer(); + $add = true; + foreach ($this->stack['abbr'] as $stacked) { + if ($stacked['text'] == $tag['text']) { + /** TODO: differing abbr definitions, i.e. different titles for same text **/ + $add = false; + break; + } + } + $this->out($tag['text']); + if ($add) { + array_push($this->stack['abbr'], $tag); + } + } + } + /** + * flush stacked abbr tags + * + * @param void + * @return void + */ + function flushStacked_abbr() { + $out = array(); + foreach ($this->stack['abbr'] as $k => $tag) { + if (!isset($tag['unstacked'])) { + array_push($out, ' *['.$tag['text'].']: '.$tag['title']); + $tag['unstacked'] = true; + $this->stack['abbr'][$k] = $tag; + } + } + if (!empty($out)) { + $this->out("\n\n".implode("\n", $out)); + } + } + /** + * handle <table> tags + * + * @param void + * @return void + */ + function handleTag_table() { + if ($this->parser->isStartTag) { + # check if upcoming table can be converted + if ($this->keepHTML) { + if (preg_match($this->tableLookaheadHeader, $this->parser->html, $matches)) { + # header seems good, now check body + # get align & number of cols + preg_match_all('#<th(?:\s+align=("|\')(left|right|center)\1)?\s*>#si', $matches[0], $cols); + $regEx = ''; + $i = 1; + $aligns = array(); + foreach ($cols[2] as $align) { + $align = strtolower($align); + array_push($aligns, $align); + if (empty($align)) { + $align = 'left'; # default value + } + $td = '\s+align=("|\')'.$align.'\\'.$i; + $i++; + if ($align == 'left') { + # look for empty align or left + $td = '(?:'.$td.')?'; + } + $td = '<td'.$td.'\s*>'; + $regEx .= $td.$this->tdSubstitute; + } + $regEx = sprintf($this->tableLookaheadBody, $regEx); + if (preg_match($regEx, $this->parser->html, $matches, null, strlen($matches[0]))) { + # this is a markdownable table tag! + $this->table = array( + 'rows' => array(), + 'col_widths' => array(), + 'aligns' => $aligns, + ); + $this->row = 0; + } else { + # non markdownable table + $this->handleTagToText(); + } + } else { + # non markdownable table + $this->handleTagToText(); + } + } else { + $this->table = array( + 'rows' => array(), + 'col_widths' => array(), + 'aligns' => array(), + ); + $this->row = 0; + } + } else { + # finally build the table in Markdown Extra syntax + $separator = array(); + # seperator with correct align identifikators + foreach($this->table['aligns'] as $col => $align) { + if (!$this->keepHTML && !isset($this->table['col_widths'][$col])) { + break; + } + $left = ' '; + $right = ' '; + switch ($align) { + case 'left': + $left = ':'; + break; + case 'center': + $right = ':'; + $left = ':'; + case 'right': + $right = ':'; + break; + } + array_push($separator, $left.str_repeat('-', $this->table['col_widths'][$col]).$right); + } + $separator = '|'.implode('|', $separator).'|'; + + $rows = array(); + # add padding + array_walk_recursive($this->table['rows'], array(&$this, 'alignTdContent')); + $header = array_shift($this->table['rows']); + array_push($rows, '| '.implode(' | ', $header).' |'); + array_push($rows, $separator); + foreach ($this->table['rows'] as $row) { + array_push($rows, '| '.implode(' | ', $row).' |'); + } + $this->out(implode("\n".$this->indent, $rows)); + $this->table = array(); + $this->setLineBreaks(2); + } + } + /** + * properly pad content so it is aligned as whished + * should be used with array_walk_recursive on $this->table['rows'] + * + * @param string &$content + * @param int $col + * @return void + */ + function alignTdContent(&$content, $col) { + switch ($this->table['aligns'][$col]) { + default: + case 'left': + $content .= str_repeat(' ', $this->table['col_widths'][$col] - $this->strlen($content)); + break; + case 'right': + $content = str_repeat(' ', $this->table['col_widths'][$col] - $this->strlen($content)).$content; + break; + case 'center': + $paddingNeeded = $this->table['col_widths'][$col] - $this->strlen($content); + $left = floor($paddingNeeded / 2); + $right = $paddingNeeded - $left; + $content = str_repeat(' ', $left).$content.str_repeat(' ', $right); + break; + } + } + /** + * handle <tr> tags + * + * @param void + * @return void + */ + function handleTag_tr() { + if ($this->parser->isStartTag) { + $this->col = -1; + } else { + $this->row++; + } + } + /** + * handle <td> tags + * + * @param void + * @return void + */ + function handleTag_td() { + if ($this->parser->isStartTag) { + $this->col++; + if (!isset($this->table['col_widths'][$this->col])) { + $this->table['col_widths'][$this->col] = 0; + } + $this->buffer(); + } else { + $buffer = trim($this->unbuffer()); + $this->table['col_widths'][$this->col] = max($this->table['col_widths'][$this->col], $this->strlen($buffer)); + $this->table['rows'][$this->row][$this->col] = $buffer; + } + } + /** + * handle <th> tags + * + * @param void + * @return void + */ + function handleTag_th() { + if (!$this->keepHTML && !isset($this->table['rows'][1]) && !isset($this->table['aligns'][$this->col+1])) { + if (isset($this->parser->tagAttributes['align'])) { + $this->table['aligns'][$this->col+1] = $this->parser->tagAttributes['align']; + } else { + $this->table['aligns'][$this->col+1] = ''; + } + } + $this->handleTag_td(); + } + /** + * handle <dl> tags + * + * @param void + * @return void + */ + function handleTag_dl() { + if (!$this->parser->isStartTag) { + $this->setLineBreaks(2); + } + } + /** + * handle <dt> tags + * + * @param void + * @return void + **/ + function handleTag_dt() { + if (!$this->parser->isStartTag) { + $this->setLineBreaks(1); + } + } + /** + * handle <dd> tags + * + * @param void + * @return void + */ + function handleTag_dd() { + if ($this->parser->isStartTag) { + if (substr(ltrim($this->parser->html), 0, 3) == '<p>') { + # next comes a paragraph, so we'll need an extra line + $this->out("\n".$this->indent); + } elseif (substr($this->output, -2) == "\n\n") { + $this->output = substr($this->output, 0, -1); + } + $this->out(': '); + $this->indent(' ', false); + } else { + # lookahead for next dt + if (substr(ltrim($this->parser->html), 0, 4) == '<dt>') { + $this->setLineBreaks(2); + } else { + $this->setLineBreaks(1); + } + $this->indent(' '); + } + } + /** + * handle <fnref /> tags (custom footnote references, see markdownify_extra::parseString()) + * + * @param void + * @return void + */ + function handleTag_fnref() { + $this->out('[^'.$this->parser->tagAttributes['target'].']'); + } + /** + * handle <fn> tags (custom footnotes, see markdownify_extra::parseString() + * and markdownify_extra::_makeFootnotes()) + * + * @param void + * @return void + */ + function handleTag_fn() { + if ($this->parser->isStartTag) { + $this->out('[^'.$this->parser->tagAttributes['name'].']:'); + $this->setLineBreaks(1); + } else { + $this->setLineBreaks(2); + } + $this->indent(' '); + } + /** + * handle <footnotes> tag (custom footnotes, see markdownify_extra::parseString() + * and markdownify_extra::_makeFootnotes()) + * + * @param void + * @return void + */ + function handleTag_footnotes() { + if (!$this->parser->isStartTag) { + $this->setLineBreaks(2); + } + } + /** + * parse a HTML string, clean up footnotes prior + * + * @param string $HTML input + * @return string Markdown formatted output + */ + function parseString($html) { + /** TODO: custom markdown-extra options, e.g. titles & classes **/ + # <sup id="fnref:..."><a href"#fn..." rel="footnote">...</a></sup> + # => <fnref target="..." /> + $html = preg_replace('@<sup id="fnref:([^"]+)">\s*<a href="#fn:\1" rel="footnote">\s*\d+\s*</a>\s*</sup>@Us', '<fnref target="$1" />', $html); + # <div class="footnotes"> + # <hr /> + # <ol> + # + # <li id="fn:...">...</li> + # ... + # + # </ol> + # </div> + # => + # <footnotes> + # <fn name="...">...</fn> + # ... + # </footnotes> + $html = preg_replace_callback('#<div class="footnotes">\s*<hr />\s*<ol>\s*(.+)\s*</ol>\s*</div>#Us', array(&$this, '_makeFootnotes'), $html); + return parent::parseString($html); + } + /** + * replace HTML representation of footnotes with something more easily parsable + * + * @note this is a callback to be used in parseString() + * + * @param array $matches + * @return string + */ + function _makeFootnotes($matches) { + # <li id="fn:1"> + # ... + # <a href="#fnref:block" rev="footnote">↩</a></p> + # </li> + # => <fn name="1">...</fn> + # remove footnote link + $fns = preg_replace('@\s*( \s*)?<a href="#fnref:[^"]+" rev="footnote"[^>]*>↩</a>\s*@s', '', $matches[1]); + # remove empty paragraph + $fns = preg_replace('@<p>\s*</p>@s', '', $fns); + # <li id="fn:1">...</li> -> <footnote nr="1">...</footnote> + $fns = str_replace('<li id="fn:', '<fn name="', $fns); + + $fns = '<footnotes>'.$fns.'</footnotes>'; + return preg_replace('#</li>\s*(?=(?:<fn|</footnotes>))#s', '</fn>$1', $fns); + } +}
\ No newline at end of file diff --git a/include/markdownify/parsehtml/parsehtml.php b/include/markdownify/parsehtml/parsehtml.php new file mode 100644 index 000000000..1a8ecacda --- /dev/null +++ b/include/markdownify/parsehtml/parsehtml.php @@ -0,0 +1,618 @@ +<?php +/** + * parseHTML is a HTML parser which works with PHP 4 and above. + * It tries to handle invalid HTML to some degree. + * + * @version 1.0 beta + * @author Milian Wolff (mail@milianw.de, http://milianw.de) + * @license LGPL, see LICENSE_LGPL.txt and the summary below + * @copyright (C) 2007 Milian Wolff + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +class parseHTML { + /** + * tags which are always empty (<br /> etc.) + * + * @var array<string> + */ + var $emptyTags = array( + 'br', + 'hr', + 'input', + 'img', + 'area', + 'link', + 'meta', + 'param', + ); + /** + * tags with preformatted text + * whitespaces wont be touched in them + * + * @var array<string> + */ + var $preformattedTags = array( + 'script', + 'style', + 'pre', + 'code', + ); + /** + * supress HTML tags inside preformatted tags (see above) + * + * @var bool + */ + var $noTagsInCode = false; + /** + * html to be parsed + * + * @var string + */ + var $html = ''; + /** + * node type: + * + * - tag (see isStartTag) + * - text (includes cdata) + * - comment + * - doctype + * - pi (processing instruction) + * + * @var string + */ + var $nodeType = ''; + /** + * current node content, i.e. either a + * simple string (text node), or something like + * <tag attrib="value"...> + * + * @var string + */ + var $node = ''; + /** + * wether current node is an opening tag (<a>) or not (</a>) + * set to NULL if current node is not a tag + * NOTE: empty tags (<br />) set this to true as well! + * + * @var bool | null + */ + var $isStartTag = null; + /** + * wether current node is an empty tag (<br />) or not (<a></a>) + * + * @var bool | null + */ + var $isEmptyTag = null; + /** + * tag name + * + * @var string | null + */ + var $tagName = ''; + /** + * attributes of current tag + * + * @var array (attribName=>value) | null + */ + var $tagAttributes = null; + /** + * wether the current tag is a block element + * + * @var bool | null + */ + var $isBlockElement = null; + + /** + * keep whitespace + * + * @var int + */ + var $keepWhitespace = 0; + /** + * list of open tags + * count this to get current depth + * + * @var array + */ + var $openTags = array(); + /** + * list of block elements + * + * @var array + * TODO: what shall we do with <del> and <ins> ?! + */ + var $blockElements = array ( + # tag name => <bool> is block + # block elements + 'address' => true, + 'blockquote' => true, + 'center' => true, + 'del' => true, + 'dir' => true, + 'div' => true, + 'dl' => true, + 'fieldset' => true, + 'form' => true, + 'h1' => true, + 'h2' => true, + 'h3' => true, + 'h4' => true, + 'h5' => true, + 'h6' => true, + 'hr' => true, + 'ins' => true, + 'isindex' => true, + 'menu' => true, + 'noframes' => true, + 'noscript' => true, + 'ol' => true, + 'p' => true, + 'pre' => true, + 'table' => true, + 'ul' => true, + # set table elements and list items to block as well + 'thead' => true, + 'tbody' => true, + 'tfoot' => true, + 'td' => true, + 'tr' => true, + 'th' => true, + 'li' => true, + 'dd' => true, + 'dt' => true, + # header items and html / body as well + 'html' => true, + 'body' => true, + 'head' => true, + 'meta' => true, + 'link' => true, + 'style' => true, + 'title' => true, + # unfancy media tags, when indented should be rendered as block + 'map' => true, + 'object' => true, + 'param' => true, + 'embed' => true, + 'area' => true, + # inline elements + 'a' => false, + 'abbr' => false, + 'acronym' => false, + 'applet' => false, + 'b' => false, + 'basefont' => false, + 'bdo' => false, + 'big' => false, + 'br' => false, + 'button' => false, + 'cite' => false, + 'code' => false, + 'del' => false, + 'dfn' => false, + 'em' => false, + 'font' => false, + 'i' => false, + 'img' => false, + 'ins' => false, + 'input' => false, + 'iframe' => false, + 'kbd' => false, + 'label' => false, + 'q' => false, + 'samp' => false, + 'script' => false, + 'select' => false, + 'small' => false, + 'span' => false, + 'strong' => false, + 'sub' => false, + 'sup' => false, + 'textarea' => false, + 'tt' => false, + 'var' => false, + ); + /** + * get next node, set $this->html prior! + * + * @param void + * @return bool + */ + function nextNode() { + if (empty($this->html)) { + # we are done with parsing the html string + return false; + } + static $skipWhitespace = true; + if ($this->isStartTag && !$this->isEmptyTag) { + array_push($this->openTags, $this->tagName); + if (in_array($this->tagName, $this->preformattedTags)) { + # dont truncate whitespaces for <code> or <pre> contents + $this->keepWhitespace++; + } + } + + if ($this->html[0] == '<') { + $token = substr($this->html, 0, 9); + if (substr($token, 0, 2) == '<?') { + # xml prolog or other pi's + /** TODO **/ + #trigger_error('this might need some work', E_USER_NOTICE); + $pos = strpos($this->html, '>'); + $this->setNode('pi', $pos + 1); + return true; + } + if (substr($token, 0, 4) == '<!--') { + # comment + $pos = strpos($this->html, '-->'); + if ($pos === false) { + # could not find a closing -->, use next gt instead + # this is firefox' behaviour + $pos = strpos($this->html, '>') + 1; + } else { + $pos += 3; + } + $this->setNode('comment', $pos); + + $skipWhitespace = true; + return true; + } + if ($token == '<!DOCTYPE') { + # doctype + $this->setNode('doctype', strpos($this->html, '>')+1); + + $skipWhitespace = true; + return true; + } + if ($token == '<![CDATA[') { + # cdata, use text node + + # remove leading <![CDATA[ + $this->html = substr($this->html, 9); + + $this->setNode('text', strpos($this->html, ']]>')+3); + + # remove trailing ]]> and trim + $this->node = substr($this->node, 0, -3); + $this->handleWhitespaces(); + + $skipWhitespace = true; + return true; + } + if ($this->parseTag()) { + # seems to be a tag + # handle whitespaces + if ($this->isBlockElement) { + $skipWhitespace = true; + } else { + $skipWhitespace = false; + } + return true; + } + } + if ($this->keepWhitespace) { + $skipWhitespace = false; + } + # when we get here it seems to be a text node + $pos = strpos($this->html, '<'); + if ($pos === false) { + $pos = strlen($this->html); + } + $this->setNode('text', $pos); + $this->handleWhitespaces(); + if ($skipWhitespace && $this->node == ' ') { + return $this->nextNode(); + } + $skipWhitespace = false; + return true; + } + /** + * parse tag, set tag name and attributes, see if it's a closing tag and so forth... + * + * @param void + * @return bool + */ + function parseTag() { + static $a_ord, $z_ord, $special_ords; + if (!isset($a_ord)) { + $a_ord = ord('a'); + $z_ord = ord('z'); + $special_ords = array( + ord(':'), // for xml:lang + ord('-'), // for http-equiv + ); + } + + $tagName = ''; + + $pos = 1; + $isStartTag = $this->html[$pos] != '/'; + if (!$isStartTag) { + $pos++; + } + # get tagName + while (isset($this->html[$pos])) { + $pos_ord = ord(strtolower($this->html[$pos])); + if (($pos_ord >= $a_ord && $pos_ord <= $z_ord) || (!empty($tagName) && is_numeric($this->html[$pos]))) { + $tagName .= $this->html[$pos]; + $pos++; + } else { + $pos--; + break; + } + } + + $tagName = strtolower($tagName); + if (empty($tagName) || !isset($this->blockElements[$tagName])) { + # something went wrong => invalid tag + $this->invalidTag(); + return false; + } + if ($this->noTagsInCode && end($this->openTags) == 'code' && !($tagName == 'code' && !$isStartTag)) { + # we supress all HTML tags inside code tags + $this->invalidTag(); + return false; + } + + # get tag attributes + /** TODO: in html 4 attributes do not need to be quoted **/ + $isEmptyTag = false; + $attributes = array(); + $currAttrib = ''; + while (isset($this->html[$pos+1])) { + $pos++; + # close tag + if ($this->html[$pos] == '>' || $this->html[$pos].$this->html[$pos+1] == '/>') { + if ($this->html[$pos] == '/') { + $isEmptyTag = true; + $pos++; + } + break; + } + + $pos_ord = ord(strtolower($this->html[$pos])); + if ( ($pos_ord >= $a_ord && $pos_ord <= $z_ord) || in_array($pos_ord, $special_ords)) { + # attribute name + $currAttrib .= $this->html[$pos]; + } elseif (in_array($this->html[$pos], array(' ', "\t", "\n"))) { + # drop whitespace + } elseif (in_array($this->html[$pos].$this->html[$pos+1], array('="', "='"))) { + # get attribute value + $pos++; + $await = $this->html[$pos]; # single or double quote + $pos++; + $value = ''; + while (isset($this->html[$pos]) && $this->html[$pos] != $await) { + $value .= $this->html[$pos]; + $pos++; + } + $attributes[$currAttrib] = $value; + $currAttrib = ''; + } else { + $this->invalidTag(); + return false; + } + } + if ($this->html[$pos] != '>') { + $this->invalidTag(); + return false; + } + + if (!empty($currAttrib)) { + # html 4 allows something like <option selected> instead of <option selected="selected"> + $attributes[$currAttrib] = $currAttrib; + } + if (!$isStartTag) { + if (!empty($attributes) || $tagName != end($this->openTags)) { + # end tags must not contain any attributes + # or maybe we did not expect a different tag to be closed + $this->invalidTag(); + return false; + } + array_pop($this->openTags); + if (in_array($tagName, $this->preformattedTags)) { + $this->keepWhitespace--; + } + } + $pos++; + $this->node = substr($this->html, 0, $pos); + $this->html = substr($this->html, $pos); + $this->tagName = $tagName; + $this->tagAttributes = $attributes; + $this->isStartTag = $isStartTag; + $this->isEmptyTag = $isEmptyTag || in_array($tagName, $this->emptyTags); + if ($this->isEmptyTag) { + # might be not well formed + $this->node = preg_replace('# */? *>$#', ' />', $this->node); + } + $this->nodeType = 'tag'; + $this->isBlockElement = $this->blockElements[$tagName]; + return true; + } + /** + * handle invalid tags + * + * @param void + * @return void + */ + function invalidTag() { + $this->html = substr_replace($this->html, '<', 0, 1); + } + /** + * update all vars and make $this->html shorter + * + * @param string $type see description for $this->nodeType + * @param int $pos to which position shall we cut? + * @return void + */ + function setNode($type, $pos) { + if ($this->nodeType == 'tag') { + # set tag specific vars to null + # $type == tag should not be called here + # see this::parseTag() for more + $this->tagName = null; + $this->tagAttributes = null; + $this->isStartTag = null; + $this->isEmptyTag = null; + $this->isBlockElement = null; + + } + $this->nodeType = $type; + $this->node = substr($this->html, 0, $pos); + $this->html = substr($this->html, $pos); + } + /** + * check if $this->html begins with $str + * + * @param string $str + * @return bool + */ + function match($str) { + return substr($this->html, 0, strlen($str)) == $str; + } + /** + * truncate whitespaces + * + * @param void + * @return void + */ + function handleWhitespaces() { + if ($this->keepWhitespace) { + # <pre> or <code> before... + return; + } + # truncate multiple whitespaces to a single one + $this->node = preg_replace('#\s+#s', ' ', $this->node); + } + /** + * normalize self::node + * + * @param void + * @return void + */ + function normalizeNode() { + $this->node = '<'; + if (!$this->isStartTag) { + $this->node .= '/'.$this->tagName.'>'; + return; + } + $this->node .= $this->tagName; + foreach ($this->tagAttributes as $name => $value) { + $this->node .= ' '.$name.'="'.str_replace('"', '"', $value).'"'; + } + if ($this->isEmptyTag) { + $this->node .= ' /'; + } + $this->node .= '>'; + } +} + +/** + * indent a HTML string properly + * + * @param string $html + * @param string $indent optional + * @return string + */ +function indentHTML($html, $indent = " ", $noTagsInCode = false) { + $parser = new parseHTML; + $parser->noTagsInCode = $noTagsInCode; + $parser->html = $html; + $html = ''; + $last = true; # last tag was block elem + $indent_a = array(); + while($parser->nextNode()) { + if ($parser->nodeType == 'tag') { + $parser->normalizeNode(); + } + if ($parser->nodeType == 'tag' && $parser->isBlockElement) { + $isPreOrCode = in_array($parser->tagName, array('code', 'pre')); + if (!$parser->keepWhitespace && !$last && !$isPreOrCode) { + $html = rtrim($html)."\n"; + } + if ($parser->isStartTag) { + $html .= implode($indent_a); + if (!$parser->isEmptyTag) { + array_push($indent_a, $indent); + } + } else { + array_pop($indent_a); + if (!$isPreOrCode) { + $html .= implode($indent_a); + } + } + $html .= $parser->node; + if (!$parser->keepWhitespace && !($isPreOrCode && $parser->isStartTag)) { + $html .= "\n"; + } + $last = true; + } else { + if ($parser->nodeType == 'tag' && $parser->tagName == 'br') { + $html .= $parser->node."\n"; + $last = true; + continue; + } elseif ($last && !$parser->keepWhitespace) { + $html .= implode($indent_a); + $parser->node = ltrim($parser->node); + } + $html .= $parser->node; + + if (in_array($parser->nodeType, array('comment', 'pi', 'doctype'))) { + $html .= "\n"; + } else { + $last = false; + } + } + } + return $html; +} +/* +# testcase / example +error_reporting(E_ALL); + +$html = '<p>Simple block on one line:</p> + +<div>foo</div> + +<p>And nested without indentation:</p> + +<div> +<div> +<div> +foo +</div> +<div style=">"/> +</div> +<div>bar</div> +</div> + +<p>And with attributes:</p> + +<div> + <div id="foo"> + </div> +</div> + +<p>This was broken in 1.0.2b7:</p> + +<div class="inlinepage"> +<div class="toggleableend"> +foo +</div> +</div>'; +#$html = '<a href="asdfasdf" title=\'asdf\' foo="bar">asdf</a>'; +echo indentHTML($html); +die(); +*/ diff --git a/include/nav.php b/include/nav.php index a67a8b614..e26cc8889 100644 --- a/include/nav.php +++ b/include/nav.php @@ -162,6 +162,7 @@ function nav(&$a) { $tpl = get_markup_template('nav.tpl'); $a->page['nav'] .= replace_macros($tpl, array( + '$baseurl' => $a->get_baseurl(), '$langselector' => lang_selector(), '$sitelocation' => $sitelocation, '$nav' => $nav, diff --git a/include/network.php b/include/network.php index 446413cd8..9e6f8355b 100644 --- a/include/network.php +++ b/include/network.php @@ -607,6 +607,9 @@ function validate_url(&$url) { if(! function_exists('validate_email')) { function validate_email($addr) { + if(get_config('system','disable_email_validation')) + return true; + if(! strpos($addr,'@')) return false; $h = substr($addr,strpos($addr,'@') + 1); @@ -793,7 +796,7 @@ function add_fcontact($arr,$update = false) { } -function scale_external_images($s,$include_link = true) { +function scale_external_images($s, $include_link = true, $scale_replace = false) { $a = get_app(); @@ -803,10 +806,22 @@ function scale_external_images($s,$include_link = true) { require_once('include/Photo.php'); foreach($matches as $mtch) { logger('scale_external_image: ' . $mtch[1]); + $hostname = str_replace('www.','',substr($a->get_baseurl(),strpos($a->get_baseurl(),'://')+3)); if(stristr($mtch[1],$hostname)) continue; - $i = fetch_url($mtch[1]); + + // $scale_replace, if passed, is an array of two elements. The + // first is the name of the full-size image. The second is the + // name of a remote, scaled-down version of the full size image. + // This allows Friendica to display the smaller remote image if + // one exists, while still linking to the full-size image + if($scale_replace) + $scaled = str_replace($scale_replace[0], $scale_replace[1], $mtch[1]); + else + $scaled = $mtch[1]; + $i = fetch_url($scaled); + // guess mimetype from headers or filename $type = guess_image_type($mtch[1],true); @@ -822,7 +837,7 @@ function scale_external_images($s,$include_link = true) { $new_width = $ph->getWidth(); $new_height = $ph->getHeight(); logger('scale_external_images: ' . $orig_width . '->' . $new_width . 'w ' . $orig_height . '->' . $new_height . 'h' . ' match: ' . $mtch[0], LOGGER_DEBUG); - $s = str_replace($mtch[0],'[img=' . $new_width . 'x' . $new_height. ']' . $mtch[1] . '[/img]' + $s = str_replace($mtch[0],'[img=' . $new_width . 'x' . $new_height. ']' . $scaled . '[/img]' . "\n" . (($include_link) ? '[url=' . $mtch[1] . ']' . t('view full size') . '[/url]' . "\n" : ''),$s); diff --git a/include/notifier.php b/include/notifier.php index f0a1940d4..f54efba31 100644 --- a/include/notifier.php +++ b/include/notifier.php @@ -125,7 +125,7 @@ function notifier_run($argv, $argc){ $uid = $r[0]['uid']; $updated = $r[0]['edited']; - // The following seems superfluous. We've already checked for "if (! intval($r[0]['parent']))" a few lines up + // POSSIBLE CLEANUP --> The following seems superfluous. We've already checked for "if (! intval($r[0]['parent']))" a few lines up if(! $parent_id) return; @@ -399,7 +399,7 @@ function notifier_run($argv, $argc){ // private emails may be in included in public conversations. Filter them. - if(($public_message) && $item['private']) + if(($public_message) && $item['private'] == 1) continue; diff --git a/include/oembed.php b/include/oembed.php index e2504b7eb..a4452586e 100755 --- a/include/oembed.php +++ b/include/oembed.php @@ -93,7 +93,8 @@ function oembed_format_object($j){ $ret.="<br>"; }; break; case "photo": { - $ret.= "<img width='".$j->width."' height='".$j->height."' src='".$j->url."'>"; + $ret.= "<img width='".$j->width."' src='".$j->url."'>"; + //$ret.= "<img width='".$j->width."' height='".$j->height."' src='".$j->url."'>"; $ret.="<br>"; }; break; case "link": { diff --git a/include/onepoll.php b/include/onepoll.php index d68f26883..09e7bb763 100644 --- a/include/onepoll.php +++ b/include/onepoll.php @@ -449,7 +449,7 @@ function onepoll_run($argv, $argc){ if($xml) { logger('poller: received xml : ' . $xml, LOGGER_DATA); - if((! strstr($xml,'<?xml')) && (! strstr($xml,'<rss'))) { + if((! strstr($xml,'<?xml')) && (! strstr($xml,'<rss'))) { logger('poller: post_handshake: response from ' . $url . ' did not contain XML.'); $r = q("UPDATE `contact` SET `last-update` = '%s' WHERE `id` = %d LIMIT 1", dbesc(datetime_convert()), diff --git a/include/plugin.php b/include/plugin.php index c6b61ae6e..ffa562273 100644 --- a/include/plugin.php +++ b/include/plugin.php @@ -316,3 +316,87 @@ function get_theme_screenshot($theme) { } return($a->get_baseurl() . '/images/blank.png'); } + + +// check service_class restrictions. If there are no service_classes defined, everything is allowed. +// if $usage is supplied, we check against a maximum count and return true if the current usage is +// less than the subscriber plan allows. Otherwise we return boolean true or false if the property +// is allowed (or not) in this subscriber plan. An unset property for this service plan means +// the property is allowed, so it is only necessary to provide negative properties for each plan, +// or what the subscriber is not allowed to do. + + +function service_class_allows($uid,$property,$usage = false) { + + if($uid == local_user()) { + $service_class = $a->user['service_class']; + } + else { + $r = q("select service_class from user where uid = %d limit 1", + intval($uid) + ); + if($r !== false and count($r)) { + $service_class = $r[0]['service_class']; + } + } + if(! x($service_class)) + return true; // everything is allowed + + $arr = get_config('service_class',$service_class); + if(! is_array($arr) || (! count($arr))) + return true; + + if($usage === false) + return ((x($arr[$property])) ? (bool) $arr['property'] : true); + else { + if(! array_key_exists($property,$arr)) + return true; + return (((intval($usage)) < intval($arr[$property])) ? true : false); + } +} + + +function service_class_fetch($uid,$property) { + + if($uid == local_user()) { + $service_class = $a->user['service_class']; + } + else { + $r = q("select service_class from user where uid = %d limit 1", + intval($uid) + ); + if($r !== false and count($r)) { + $service_class = $r[0]['service_class']; + } + } + if(! x($service_class)) + return false; // everything is allowed + + $arr = get_config('service_class',$service_class); + if(! is_array($arr) || (! count($arr))) + return false; + + return((array_key_exists($property,$arr)) ? $arr[$property] : false); + +} + +function upgrade_link($bbcode = false) { + $l = get_config('service_class','upgrade_link'); + if(! $l) + return ''; + if($bbcode) + $t = sprintf('[url=%s]' . t('Click here to upgrade.') . '[/url]', $l); + else + $t = sprintf('<a href="%s">' . t('Click here to upgrade.') . '</div>', $l); + return $t; +} + +function upgrade_message($bbcode = false) { + $x = upgrade_link($bbcode); + return t('This action exceeds the limits set by your subscription plan.') . (($x) ? ' ' . $x : '') ; +} + +function upgrade_bool_message($bbcode = false) { + $x = upgrade_link($bbcode); + return t('This action is not available under your subscription plan.') . (($x) ? ' ' . $x : '') ; +} diff --git a/include/profile_advanced.php b/include/profile_advanced.php index ffb45090b..8dfb1beec 100644 --- a/include/profile_advanced.php +++ b/include/profile_advanced.php @@ -59,6 +59,11 @@ function advanced_profile(&$a) { if($txt = prepare_text($a->profile['interest'])) $profile['interest'] = array( t('Hobbies/Interests:'), $txt); + if($txt = prepare_text($a->profile['likes'])) $profile['likes'] = array( t('Likes:'), $txt); + + if($txt = prepare_text($a->profile['dislikes'])) $profile['dislikes'] = array( t('Dislikes:'), $txt); + + if($txt = prepare_text($a->profile['contact'])) $profile['contact'] = array( t('Contact information and Social Networks:'), $txt); if($txt = prepare_text($a->profile['music'])) $profile['music'] = array( t('Musical interests:'), $txt); diff --git a/include/text.php b/include/text.php index cc4bee268..409d40d59 100644 --- a/include/text.php +++ b/include/text.php @@ -656,6 +656,10 @@ function search($s,$id='search-box',$url='/search',$save = false) { if(! function_exists('valid_email')) { function valid_email($x){ + + if(get_config('system','disable_email_validation')) + return true; + if(preg_match('/^[_a-zA-Z0-9\-\+]+(\.[_a-zA-Z0-9\-\+]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$/',$x)) return true; return false; @@ -750,40 +754,40 @@ function smilies($s, $sample = false) { ); $icons = array( - '<img src="' . $a->get_baseurl() . '/images/smiley-heart.gif" alt="<3" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-brokenheart.gif" alt="</3" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-brokenheart.gif" alt="<\\3" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-smile.gif" alt=":-)" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-wink.gif" alt=";-)" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-frown.gif" alt=":-(" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-tongue-out.gif" alt=":-P" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-tongue-out.gif" alt=":-p" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-kiss.gif" alt=":-\"" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-kiss.gif" alt=":-\"" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-kiss.gif" alt=":-x" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-kiss.gif" alt=":-X" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-laughing.gif" alt=":-D" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-surprised.gif" alt="8-|" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-surprised.gif" alt="8-O" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-surprised.gif" alt=":-O" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-thumbsup.gif" alt="\\o/" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-Oo.gif" alt="o.O" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-Oo.gif" alt="O.o" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-Oo.gif" alt="o_O" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-Oo.gif" alt="O_o" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-cry.gif" alt=":\'(" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-foot-in-mouth.gif" alt=":-!" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-undecided.gif" alt=":-/" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-embarassed.gif" alt=":-[" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-cool.gif" alt="8-)" />', - '<img src="' . $a->get_baseurl() . '/images/beer_mug.gif" alt=":beer" />', - '<img src="' . $a->get_baseurl() . '/images/beer_mug.gif" alt=":homebrew" />', - '<img src="' . $a->get_baseurl() . '/images/coffee.gif" alt=":coffee" />', - '<img src="' . $a->get_baseurl() . '/images/smiley-facepalm.gif" alt=":facepalm" />', - '<img src="' . $a->get_baseurl() . '/images/like.gif" alt=":like" />', - '<img src="' . $a->get_baseurl() . '/images/dislike.gif" alt=":dislike" />', - '<a href="http://project.friendika.com">~friendika <img src="' . $a->get_baseurl() . '/images/friendika-16.png" alt="~friendika" /></a>', - '<a href="http://friendica.com">~friendica <img src="' . $a->get_baseurl() . '/images/friendica-16.png" alt="~friendica" /></a>' + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-heart.gif" alt="<3" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-brokenheart.gif" alt="</3" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-brokenheart.gif" alt="<\\3" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-smile.gif" alt=":-)" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-wink.gif" alt=";-)" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-frown.gif" alt=":-(" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-tongue-out.gif" alt=":-P" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-tongue-out.gif" alt=":-p" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-kiss.gif" alt=":-\"" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-kiss.gif" alt=":-\"" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-kiss.gif" alt=":-x" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-kiss.gif" alt=":-X" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-laughing.gif" alt=":-D" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-surprised.gif" alt="8-|" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-surprised.gif" alt="8-O" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-surprised.gif" alt=":-O" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-thumbsup.gif" alt="\\o/" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-Oo.gif" alt="o.O" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-Oo.gif" alt="O.o" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-Oo.gif" alt="o_O" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-Oo.gif" alt="O_o" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-cry.gif" alt=":\'(" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-foot-in-mouth.gif" alt=":-!" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-undecided.gif" alt=":-/" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-embarassed.gif" alt=":-[" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-cool.gif" alt="8-)" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/beer_mug.gif" alt=":beer" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/beer_mug.gif" alt=":homebrew" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/coffee.gif" alt=":coffee" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-facepalm.gif" alt=":facepalm" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/like.gif" alt=":like" />', + '<img class="smiley" src="' . $a->get_baseurl() . '/images/dislike.gif" alt=":dislike" />', + '<a href="http://project.friendika.com">~friendika <img class="smiley" src="' . $a->get_baseurl() . '/images/friendika-16.png" alt="~friendika" /></a>', + '<a href="http://friendica.com">~friendica <img class="smiley" src="' . $a->get_baseurl() . '/images/friendica-16.png" alt="~friendica" /></a>' ); $params = array('texts' => $texts, 'icons' => $icons, 'string' => $s); @@ -823,7 +827,7 @@ function preg_heart($x) { return $x[0]; $t = ''; for($cnt = 0; $cnt < strlen($x[1]); $cnt ++) - $t .= '<img src="' . $a->get_baseurl() . '/images/smiley-heart.gif" alt="<3" />'; + $t .= '<img class="smiley" src="' . $a->get_baseurl() . '/images/smiley-heart.gif" alt="<3" />'; $r = str_replace($x[0],$t,$x[0]); return $r; } @@ -1059,12 +1063,13 @@ function feed_salmonlinks($nick) { if(! function_exists('get_plink')) { function get_plink($item) { $a = get_app(); - if (x($item,'plink') && ((! $item['private']) || ($item['network'] === NETWORK_FEED))){ + if (x($item,'plink') && ($item['private'] != 1)) { return array( 'href' => $item['plink'], 'title' => t('link to source'), ); - } else { + } + else { return false; } }} @@ -1532,7 +1537,6 @@ function undo_post_tagging($s) { function fix_mce_lf($s) { $s = str_replace("\r\n","\n",$s); - $s = str_replace("\n\n","\n",$s); return $s; } diff --git a/include/user.php b/include/user.php index 2477438bf..039b30bbd 100644 --- a/include/user.php +++ b/include/user.php @@ -99,11 +99,11 @@ function create_user($arr) { if(! allowed_email($email)) - $result['message'] .= t('Your email domain is not among those allowed on this site.') . EOL; + $result['message'] .= t('Your email domain is not among those allowed on this site.') . EOL; if((! valid_email($email)) || (! validate_email($email))) $result['message'] .= t('Not a valid email address.') . EOL; - + // Disallow somebody creating an account using openid that uses the admin email address, // since openid bypasses email verification. We'll allow it if there is not yet an admin account. @@ -147,13 +147,18 @@ function create_user($arr) { require_once('include/crypto.php'); - $keys = new_keypair(1024); + $keys = new_keypair(4096); if($keys === false) { $result['message'] .= t('SERIOUS ERROR: Generation of security keys failed.') . EOL; return $result; } + $default_service_class = get_config('system','default_service_class'); + if(! $default_service_class) + $default_service_class = ''; + + $prvkey = $keys['prvkey']; $pubkey = $keys['pubkey']; @@ -173,8 +178,8 @@ function create_user($arr) { $spubkey = $sres['pubkey']; $r = q("INSERT INTO `user` ( `guid`, `username`, `password`, `email`, `openid`, `nickname`, - `pubkey`, `prvkey`, `spubkey`, `sprvkey`, `register_date`, `verified`, `blocked`, `timezone` ) - VALUES ( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, 'UTC' )", + `pubkey`, `prvkey`, `spubkey`, `sprvkey`, `register_date`, `verified`, `blocked`, `timezone`, `service_class` ) + VALUES ( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, 'UTC', '%s' )", dbesc(generate_user_guid()), dbesc($username), dbesc($new_password_encoded), @@ -187,7 +192,8 @@ function create_user($arr) { dbesc($sprvkey), dbesc(datetime_convert()), intval($verified), - intval($blocked) + intval($blocked), + dbesc($default_service_class) ); if($r) { |