Merge branch 'develop' into rewrites/coding-convention-split2

This commit is contained in:
Roland Häder 2017-04-01 22:16:08 +02:00 committed by GitHub
commit 49527b70d3
93 changed files with 11433 additions and 33261 deletions

View file

@ -37,7 +37,7 @@ local .htaccess file
- PHP *command line* access with register_argc_argv set to true in the
php.ini file [or see 'poormancron' in section 8]
- curl, gd (with at least jpeg support), mysql, mbstring, mcrypt, and openssl extensions
- curl, gd (with at least jpeg support), mysql, mbstring and openssl extensions
- some form of email server or email gateway such that PHP mail() works

View file

@ -649,7 +649,6 @@ class App {
set_include_path(
'include' . PATH_SEPARATOR
. 'library' . PATH_SEPARATOR
. 'library/phpsec' . PATH_SEPARATOR
. 'library/langdet' . PATH_SEPARATOR
. '.' );

View file

@ -30,7 +30,6 @@ Requirements
* PHP *command line* access with register_argc_argv set to true in the php.ini file
* curl, gd, mysql, hash and openssl extensions
* some form of email server or email gateway such that PHP mail() works
* mcrypt (optional; used for server-to-server message encryption)
* Mysql 5.5.3+ or an equivalant alternative for MySQL (MariaDB, Percona Server etc.)
* the ability to schedule jobs with cron (Linux/Mac) or Scheduled Tasks (Windows) (Note: other options are presented in Section 7 of this document.)
* Installation into a top-level domain or sub-domain (without a directory/path component in the URL) is preferred. Directory paths will not be as convenient to use and have not been thoroughly tested.

View file

@ -152,13 +152,6 @@ Value is in seconds.
Default is 60 seconds.
Set to 0 for unlimited (not recommended).
#### UTF-8 Regular Expressions
During registrations, full names are checked using UTF-8 regular expressions.
This requires PHP to have been compiled with a special setting to allow UTF-8 expressions.
If you are completely unable to register accounts, set no_utf to true.
The default is set to false (meaning UTF8 regular expressions are supported and working).
#### Verify SSL Certitificates
By default Friendica allows SSL communication between websites that have "self-signed" SSL certificates.

View file

@ -26,7 +26,6 @@ Wir planen, diese Einschränkung in einer zukünftigen Version zu beheben.
- PHP *Kommandozeilen*-Zugang mit register_argc_argv auf "true" gesetzt in der php.ini-Datei
- curl, gd, mysql und openssl-Erweiterung
- etwas in der Art eines Email-Servers oder eines Gateways wie PHP mail()
- mcrypt (optional; wird für die Server-zu-Server Nachrichtenentschlüsselung benötigt)
- Mysql 5.x
- die Möglichkeit, wiederkehrende Aufgaben mit cron (Linux/Mac) oder "Scheduled Tasks" einzustellen (Windows) [Beachte: andere Optionen sind in Abschnitt 7 dieser Dokumentation zu finden]
- Installation in einer Top-Level-Domain oder Subdomain (ohne eine Verzeichnis/Pfad-Komponente in der URL) wird bevorzugt. Verzeichnispfade sind für diesen Zweck nicht so günstig und wurden auch nicht ausführlich getestet.
@ -37,7 +36,7 @@ Wir planen, diese Einschränkung in einer zukünftigen Version zu beheben.
1.1. APT-Pakete
- Apache: sudo apt-get install apache2
- PHP5: sudo apt-get install php5
- PHP5-Zusätzliche Pakete: sudo apt-get install php5-curl php5-gd php5-mysql php5-mcrypt
- PHP5-Zusätzliche Pakete: sudo apt-get install php5-curl php5-gd php5-mysql
- MySQL: sudo apt-get install mysql-server
2. Entpacke die Friendica-Daten in das Quellverzeichnis (root) des Dokumentenbereichs deines Webservers.

View file

@ -65,9 +65,8 @@ $a->config['php_path'] = 'php';
$a->config['system']['huburl'] = '[internal]';
// Server-to-server private message encryption (RINO) is allowed by default.
// Encryption will only be provided if this setting is set to a non zero
// value and the PHP mcrypt extension is installed on both systems
// set to 0 to disable, 2 to enable, 1 is deprecated but wont need mcrypt
// Encryption will only be provided if this setting is set to a non zero value
// set to 0 to disable, 2 to enable, 1 is deprecated
$a->config['system']['rino_encrypt'] = 2;

View file

@ -18,6 +18,8 @@ require_once('include/network.php');
*/
class Probe {
private static $baseurl;
/**
* @brief Rearrange the array so that it always has the same order
*
@ -54,6 +56,9 @@ class Probe {
*/
private function xrd($host) {
// Reset the static variable
self::$baseurl = '';
$ssl_url = "https://".$host."/.well-known/host-meta";
$url = "http://".$host."/.well-known/host-meta";
@ -102,6 +107,9 @@ class Probe {
elseif ($attributes["rel"] == "lrdd")
$xrd_data["lrdd"] = $attributes["template"];
}
self::$baseurl = "http://".$host;
return $xrd_data;
}
@ -169,6 +177,8 @@ class Probe {
$path_parts = explode("/", trim($parts["path"], "/"));
$nick = array_pop($path_parts);
do {
$lrdd = self::xrd($host);
$host .= "/".array_shift($path_parts);
@ -192,6 +202,19 @@ class Probe {
$path = str_replace('{uri}', urlencode("acct:".$uri), $link);
$webfinger = self::webfinger($path);
}
// Special treatment for Mastodon
// Problem is that Mastodon uses an URL format like http://domain.tld/@nick
// But the webfinger for this format fails.
if (!$webfinger AND isset($nick)) {
// Mastodon uses a "@" as prefix for usernames in their url format
$nick = ltrim($nick, '@');
$addr = $nick."@".$host;
$path = str_replace('{uri}', urlencode("acct:".$addr), $link);
$webfinger = self::webfinger($path);
}
}
if (!is_array($webfinger["links"]))
@ -258,8 +281,13 @@ class Probe {
$data['nick'] = trim(substr($data['nick'], 0, strpos($data['nick'], ' ')));
}
if (!isset($data["network"]))
if (self::$baseurl != "") {
$data["baseurl"] = self::$baseurl;
}
if (!isset($data["network"])) {
$data["network"] = NETWORK_PHANTOM;
}
$data = self::rearrange_data($data);
@ -286,6 +314,7 @@ class Probe {
dbesc(normalise_link($data['url']))
);
}
return $data;
}
@ -301,7 +330,34 @@ class Probe {
* @return array uri data
*/
private function detect($uri, $network, $uid) {
if (strstr($uri, '@')) {
$parts = parse_url($uri);
if (isset($parts["scheme"]) AND isset($parts["host"]) AND isset($parts["path"])) {
/// @todo: Ports?
$host = $parts["host"];
if ($host == 'twitter.com') {
return array("network" => NETWORK_TWITTER);
}
$lrdd = self::xrd($host);
$path_parts = explode("/", trim($parts["path"], "/"));
while (!$lrdd AND (sizeof($path_parts) > 1)) {
$host .= "/".array_shift($path_parts);
$lrdd = self::xrd($host);
}
if (!$lrdd) {
return self::feed($uri);
}
$nick = array_pop($path_parts);
// Mastodon uses a "@" as prefix for usernames in their url format
$nick = ltrim($nick, '@');
$addr = $nick."@".$host;
} elseif (strstr($uri, '@')) {
// If the URI starts with "mailto:" then jump directly to the mail detection
if (strpos($url,'mailto:') !== false) {
$uri = str_replace('mailto:', '', $url);
@ -317,42 +373,19 @@ class Probe {
$host = substr($uri,strpos($uri, '@') + 1);
$nick = substr($uri,0, strpos($uri, '@'));
if (strpos($uri, '@twitter.com'))
if (strpos($uri, '@twitter.com')) {
return array("network" => NETWORK_TWITTER);
}
$lrdd = self::xrd($host);
if (!$lrdd)
if (!$lrdd) {
return self::mail($uri, $uid);
}
$addr = $uri;
} else {
$parts = parse_url($uri);
if (!isset($parts["scheme"]) OR
!isset($parts["host"]) OR
!isset($parts["path"]))
return false;
/// @todo: Ports?
$host = $parts["host"];
if ($host == 'twitter.com')
return array("network" => NETWORK_TWITTER);
$lrdd = self::xrd($host);
$path_parts = explode("/", trim($parts["path"], "/"));
while (!$lrdd AND (sizeof($path_parts) > 1)) {
$host .= "/".array_shift($path_parts);
$lrdd = self::xrd($host);
}
if (!$lrdd)
return self::feed($uri);
$nick = array_pop($path_parts);
$addr = $nick."@".$host;
return false;
}
$webfinger = false;
/// @todo Do we need the prefix "acct:" or "acct://"?

View file

@ -352,6 +352,7 @@ use \Friendica\Core\Config;
}
}
}
logger('API call not implemented: '.$a->query_string);
throw new NotImplementedException();
} catch (HTTPException $e) {
header("HTTP/1.1 {$e->httpcode} {$e->httpdesc}");
@ -2720,6 +2721,7 @@ use \Friendica\Core\Config;
return api_format_data('config', $type, array('config' => $config));
}
api_register_func('api/gnusocial/config','api_statusnet_config',false);
api_register_func('api/statusnet/config','api_statusnet_config',false);
function api_statusnet_version($type) {
@ -2728,6 +2730,7 @@ use \Friendica\Core\Config;
return api_format_data('version', $type, array('version' => $fake_statusnet_version));
}
api_register_func('api/gnusocial/version','api_statusnet_version',false);
api_register_func('api/statusnet/version','api_statusnet_version',false);
/**
@ -3963,7 +3966,7 @@ use \Friendica\Core\Config;
$multi_profiles = feature_enabled(api_user(),'multi_profiles');
$directory = get_config('system', 'directory');
// get data of the specified profile id or all profiles of the user if not specified
// get data of the specified profile id or all profiles of the user if not specified
if ($profileid != 0) {
$r = q("SELECT * FROM `profile` WHERE `uid` = %d AND `id` = %d",
intval(api_user()),
@ -3971,11 +3974,10 @@ use \Friendica\Core\Config;
// error message if specified gid is not in database
if (!dbm::is_result($r))
throw new BadRequestException("profile_id not available");
}
else
} else {
$r = q("SELECT * FROM `profile` WHERE `uid` = %d",
intval(api_user()));
}
// loop through all returned profiles and retrieve data and users
$k = 0;
foreach ($r as $rr) {
@ -4002,9 +4004,11 @@ use \Friendica\Core\Config;
}
// return settings, authenticated user and profiles data
$self = q("SELECT `nurl` FROM `contact` WHERE `uid`= %d AND `self` LIMIT 1", intval(api_user()));
$result = array('multi_profiles' => $multi_profiles ? true : false,
'global_dir' => $directory,
'friendica_owner' => api_get_user($a, intval(api_user())),
'friendica_owner' => api_get_user($a, $self[0]['nurl']),
'profiles' => $profiles);
return api_format_data("friendica_profiles", $type, array('$result' => $result));
}

View file

@ -59,15 +59,6 @@ function diaspora2bb($s) {
$s = str_replace('#', '#', $s);
$search = array(" \n", "\n ");
$replace = array("\n", "\n");
do {
$oldtext = $s;
$s = str_replace($search, $replace, $s);
} while ($oldtext != $s);
$s = str_replace("\n\n", '<br>', $s);
$s = html2bbcode($s);
// protect the recycle symbol from turning into a tag, but without unescaping angles and naked ampersands

View file

@ -1,28 +1,24 @@
<?php
use \Friendica\Core\Config;
require_once('include/photos.php');
require_once('include/user.php');
function cron_run(&$argv, &$argc){
global $a;
require_once('include/session.php');
require_once('include/datetime.php');
require_once('include/items.php');
require_once('include/Contact.php');
require_once('include/email.php');
require_once('include/socgraph.php');
require_once('mod/nodeinfo.php');
require_once('include/post_update.php');
// Poll contacts with specific parameters
if ($argc > 1) {
cron_poll_contacts($argc, $argv);
return;
}
$last = get_config('system','last_cron');
$poll_interval = intval(get_config('system','cron_interval'));
if(! $poll_interval)
if (! $poll_interval) {
$poll_interval = 10;
if($last) {
}
if ($last) {
$next = $last + ($poll_interval * 60);
if($next > time()) {
logger('cron intervall not reached');
@ -33,19 +29,16 @@ function cron_run(&$argv, &$argc){
logger('cron: start');
// run queue delivery process in the background
proc_run(PRIORITY_NEGLIGIBLE, "include/queue.php");
// run the process to discover global contacts in the background
proc_run(PRIORITY_LOW, "include/discover_poco.php");
// run the process to update locally stored global contacts in the background
proc_run(PRIORITY_LOW, "include/discover_poco.php", "checkcontact");
// Expire and remove user entries
cron_expire_and_remove_users();
proc_run(PRIORITY_MEDIUM, "include/cronjobs.php", "expire_and_remove_users");
// Check OStatus conversations
proc_run(PRIORITY_MEDIUM, "include/cronjobs.php", "ostatus_mentions");
@ -59,14 +52,22 @@ function cron_run(&$argv, &$argc){
// update nodeinfo data
proc_run(PRIORITY_LOW, "include/cronjobs.php", "nodeinfo");
// once daily run birthday_updates and then expire in background
// Clear cache entries
proc_run(PRIORITY_LOW, "include/cronjobs.php", "clear_cache");
// Repair missing Diaspora values in contacts
proc_run(PRIORITY_LOW, "include/cronjobs.php", "repair_diaspora");
// Repair entries in the database
proc_run(PRIORITY_LOW, "include/cronjobs.php", "repair_database");
// once daily run birthday_updates and then expire in background
$d1 = get_config('system','last_expire_day');
$d2 = intval(datetime_convert('UTC','UTC','now','d'));
if($d2 != intval($d1)) {
update_contact_birthdays();
proc_run(PRIORITY_LOW, "include/cronjobs.php", "update_contact_birthdays");
proc_run(PRIORITY_LOW, "include/discover_poco.php", "update_server");
@ -78,18 +79,9 @@ function cron_run(&$argv, &$argc){
proc_run(PRIORITY_MEDIUM, 'include/dbclean.php');
cron_update_photo_albums();
proc_run(PRIORITY_LOW, "include/cronjobs.php", "update_photo_albums");
}
// Clear cache entries
cron_clear_cache($a);
// Repair missing Diaspora values in contacts
cron_repair_diaspora($a);
// Repair entries in the database
cron_repair_database();
// Poll contacts
cron_poll_contacts($argc, $argv);
@ -100,39 +92,6 @@ function cron_run(&$argv, &$argc){
return;
}
/**
* @brief Update the cached values for the number of photo albums per user
*/
function cron_update_photo_albums() {
$r = q("SELECT `uid` FROM `user` WHERE NOT `account_expired` AND NOT `account_removed`");
if (!dbm::is_result($r)) {
return;
}
foreach ($r AS $user) {
photo_albums($user['uid'], true);
}
}
/**
* @brief Expire and remove user entries
*/
function cron_expire_and_remove_users() {
// expire any expired accounts
q("UPDATE user SET `account_expired` = 1 where `account_expired` = 0
AND `account_expires_on` > '%s'
AND `account_expires_on` < UTC_TIMESTAMP()", dbesc(NULL_DATE));
// delete user and contact records for recently removed accounts
$r = q("SELECT * FROM `user` WHERE `account_removed` AND `account_expires_on` < UTC_TIMESTAMP() - INTERVAL 3 DAY");
if ($r) {
foreach($r as $user) {
q("DELETE FROM `contact` WHERE `uid` = %d", intval($user['uid']));
q("DELETE FROM `user` WHERE `uid` = %d", intval($user['uid']));
}
}
}
/**
* @brief Poll contacts for unreceived messages
*
@ -145,14 +104,15 @@ function cron_poll_contacts($argc, $argv) {
$force = false;
$restart = false;
if (($argc > 1) && ($argv[1] == 'force'))
if (($argc > 1) && ($argv[1] == 'force')) {
$force = true;
}
if (($argc > 1) && ($argv[1] == 'restart')) {
$restart = true;
$generation = intval($argv[2]);
if (!$generation)
if (!$generation) {
killme();
}
}
if (($argc > 1) && intval($argv[1])) {
@ -171,9 +131,9 @@ function cron_poll_contacts($argc, $argv) {
// we are unable to match those posts with a Diaspora GUID and prevent duplicates.
$abandon_days = intval(get_config('system','account_abandon_days'));
if($abandon_days < 1)
if ($abandon_days < 1) {
$abandon_days = 0;
}
$abandon_sql = (($abandon_days)
? sprintf(" AND `user`.`login_date` > UTC_TIMESTAMP() - INTERVAL %d DAY ", intval($abandon_days))
: ''
@ -244,185 +204,44 @@ function cron_poll_contacts($argc, $argv) {
switch ($contact['priority']) {
case 5:
if(datetime_convert('UTC','UTC', 'now') > datetime_convert('UTC','UTC', $t . " + 1 month"))
if (datetime_convert('UTC','UTC', 'now') > datetime_convert('UTC','UTC', $t . " + 1 month")) {
$update = true;
}
break;
case 4:
if(datetime_convert('UTC','UTC', 'now') > datetime_convert('UTC','UTC', $t . " + 1 week"))
if (datetime_convert('UTC','UTC', 'now') > datetime_convert('UTC','UTC', $t . " + 1 week")) {
$update = true;
}
break;
case 3:
if(datetime_convert('UTC','UTC', 'now') > datetime_convert('UTC','UTC', $t . " + 1 day"))
if (datetime_convert('UTC','UTC', 'now') > datetime_convert('UTC','UTC', $t . " + 1 day")) {
$update = true;
}
break;
case 2:
if(datetime_convert('UTC','UTC', 'now') > datetime_convert('UTC','UTC', $t . " + 12 hour"))
if (datetime_convert('UTC','UTC', 'now') > datetime_convert('UTC','UTC', $t . " + 12 hour")) {
$update = true;
}
break;
case 1:
default:
if(datetime_convert('UTC','UTC', 'now') > datetime_convert('UTC','UTC', $t . " + 1 hour"))
if (datetime_convert('UTC','UTC', 'now') > datetime_convert('UTC','UTC', $t . " + 1 hour")) {
$update = true;
}
break;
}
if (!$update)
if (!$update) {
continue;
}
}
logger("Polling ".$contact["network"]." ".$contact["id"]." ".$contact["nick"]." ".$contact["name"]);
if (($contact['network'] == NETWORK_FEED) AND ($contact['priority'] <= 3)) {
proc_run(PRIORITY_MEDIUM, 'include/onepoll.php', $contact['id']);
proc_run(PRIORITY_MEDIUM, 'include/onepoll.php', intval($contact['id']));
} else {
proc_run(PRIORITY_LOW, 'include/onepoll.php', $contact['id']);
proc_run(PRIORITY_LOW, 'include/onepoll.php', intval($contact['id']));
}
}
}
}
/**
* @brief Clear cache entries
*
* @param App $a
*/
function cron_clear_cache(App $a) {
$last = get_config('system','cache_last_cleared');
if($last) {
$next = $last + (3600); // Once per hour
$clear_cache = ($next <= time());
} else
$clear_cache = true;
if (!$clear_cache)
return;
// clear old cache
Cache::clear();
// clear old item cache files
clear_cache();
// clear cache for photos
clear_cache($a->get_basepath(), $a->get_basepath()."/photo");
// clear smarty cache
clear_cache($a->get_basepath()."/view/smarty3/compiled", $a->get_basepath()."/view/smarty3/compiled");
// clear cache for image proxy
if (!get_config("system", "proxy_disabled")) {
clear_cache($a->get_basepath(), $a->get_basepath()."/proxy");
$cachetime = get_config('system','proxy_cache_time');
if (!$cachetime) $cachetime = PROXY_DEFAULT_TIME;
q('DELETE FROM `photo` WHERE `uid` = 0 AND `resource-id` LIKE "pic:%%" AND `created` < NOW() - INTERVAL %d SECOND', $cachetime);
}
// Delete the cached OEmbed entries that are older than one year
q("DELETE FROM `oembed` WHERE `created` < NOW() - INTERVAL 3 MONTH");
// Delete the cached "parse_url" entries that are older than one year
q("DELETE FROM `parsed_url` WHERE `created` < NOW() - INTERVAL 3 MONTH");
// Maximum table size in megabyte
$max_tablesize = intval(get_config('system','optimize_max_tablesize')) * 1000000;
if ($max_tablesize == 0)
$max_tablesize = 100 * 1000000; // Default are 100 MB
if ($max_tablesize > 0) {
// Minimum fragmentation level in percent
$fragmentation_level = intval(get_config('system','optimize_fragmentation')) / 100;
if ($fragmentation_level == 0)
$fragmentation_level = 0.3; // Default value is 30%
// Optimize some tables that need to be optimized
$r = q("SHOW TABLE STATUS");
foreach($r as $table) {
// Don't optimize tables that are too large
if ($table["Data_length"] > $max_tablesize)
continue;
// Don't optimize empty tables
if ($table["Data_length"] == 0)
continue;
// Calculate fragmentation
$fragmentation = $table["Data_free"] / ($table["Data_length"] + $table["Index_length"]);
logger("Table ".$table["Name"]." - Fragmentation level: ".round($fragmentation * 100, 2), LOGGER_DEBUG);
// Don't optimize tables that needn't to be optimized
if ($fragmentation < $fragmentation_level)
continue;
// So optimize it
logger("Optimize Table ".$table["Name"], LOGGER_DEBUG);
q("OPTIMIZE TABLE `%s`", dbesc($table["Name"]));
}
}
set_config('system','cache_last_cleared', time());
}
/**
* @brief Repair missing values in Diaspora contacts
*
* @param App $a
*/
function cron_repair_diaspora(App $a) {
$r = q("SELECT `id`, `url` FROM `contact`
WHERE `network` = '%s' AND (`batch` = '' OR `notify` = '' OR `poll` = '' OR pubkey = '')
ORDER BY RAND() LIMIT 50", dbesc(NETWORK_DIASPORA));
if (dbm::is_result($r)) {
foreach ($r AS $contact) {
if (poco_reachable($contact["url"])) {
$data = probe_url($contact["url"]);
if ($data["network"] == NETWORK_DIASPORA) {
logger("Repair contact ".$contact["id"]." ".$contact["url"], LOGGER_DEBUG);
q("UPDATE `contact` SET `batch` = '%s', `notify` = '%s', `poll` = '%s', pubkey = '%s' WHERE `id` = %d",
dbesc($data["batch"]), dbesc($data["notify"]), dbesc($data["poll"]), dbesc($data["pubkey"]),
intval($contact["id"]));
}
}
}
}
}
/**
* @brief Do some repairs in database entries
*
*/
function cron_repair_database() {
// Sometimes there seem to be issues where the "self" contact vanishes.
// We haven't found the origin of the problem by now.
$r = q("SELECT `uid` FROM `user` WHERE NOT EXISTS (SELECT `uid` FROM `contact` WHERE `contact`.`uid` = `user`.`uid` AND `contact`.`self`)");
if (dbm::is_result($r)) {
foreach ($r AS $user) {
logger('Create missing self contact for user '.$user['uid']);
user_create_self_contact($user['uid']);
}
}
// Set the parent if it wasn't set. (Shouldn't happen - but does sometimes)
// This call is very "cheap" so we can do it at any time without a problem
q("UPDATE `item` INNER JOIN `item` AS `parent` ON `parent`.`uri` = `item`.`parent-uri` AND `parent`.`uid` = `item`.`uid` SET `item`.`parent` = `parent`.`id` WHERE `item`.`parent` = 0");
// There was an issue where the nick vanishes from the contact table
q("UPDATE `contact` INNER JOIN `user` ON `contact`.`uid` = `user`.`uid` SET `nick` = `nickname` WHERE `self` AND `nick`=''");
// Update the global contacts for local users
$r = q("SELECT `uid` FROM `user` WHERE `verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired`");
if (dbm::is_result($r))
foreach ($r AS $user)
update_gcontact_for_user($user["uid"]);
/// @todo
/// - remove thread entries without item
/// - remove sign entries without item
/// - remove children when parent got lost
/// - set contact-id in item when not present
}

View file

@ -8,10 +8,17 @@ function cronjobs_run(&$argv, &$argc){
require_once('include/ostatus.php');
require_once('include/post_update.php');
require_once('mod/nodeinfo.php');
require_once('include/photos.php');
require_once('include/user.php');
require_once('include/socgraph.php');
require_once('include/Probe.php');
// No parameter set? So return
if ($argc <= 1)
if ($argc <= 1) {
return;
}
logger("Starting cronjob ".$argv[1], LOGGER_DEBUG);
// Check OStatus conversations
// Check only conversations with mentions (for a longer time)
@ -39,5 +46,244 @@ function cronjobs_run(&$argv, &$argc){
return;
}
// Expire and remove user entries
if ($argv[1] == 'expire_and_remove_users') {
cron_expire_and_remove_users();
return;
}
if ($argv[1] == 'update_contact_birthdays') {
update_contact_birthdays();
return;
}
if ($argv[1] == 'update_photo_albums') {
cron_update_photo_albums();
return;
}
// Clear cache entries
if ($argv[1] == 'clear_cache') {
cron_clear_cache($a);
return;
}
// Repair missing Diaspora values in contacts
if ($argv[1] == 'repair_diaspora') {
cron_repair_diaspora($a);
return;
}
// Repair entries in the database
if ($argv[1] == 'repair_database') {
cron_repair_database();
return;
}
logger("Xronjob ".$argv[1]." is unknown.", LOGGER_DEBUG);
return;
}
/**
* @brief Update the cached values for the number of photo albums per user
*/
function cron_update_photo_albums() {
$r = q("SELECT `uid` FROM `user` WHERE NOT `account_expired` AND NOT `account_removed`");
if (!dbm::is_result($r)) {
return;
}
foreach ($r AS $user) {
photo_albums($user['uid'], true);
}
}
/**
* @brief Expire and remove user entries
*/
function cron_expire_and_remove_users() {
// expire any expired accounts
q("UPDATE user SET `account_expired` = 1 where `account_expired` = 0
AND `account_expires_on` > '%s'
AND `account_expires_on` < UTC_TIMESTAMP()", dbesc(NULL_DATE));
// delete user and contact records for recently removed accounts
$r = q("SELECT * FROM `user` WHERE `account_removed` AND `account_expires_on` < UTC_TIMESTAMP() - INTERVAL 3 DAY");
if (dbm::is_result($r)) {
foreach ($r as $user) {
q("DELETE FROM `contact` WHERE `uid` = %d", intval($user['uid']));
q("DELETE FROM `user` WHERE `uid` = %d", intval($user['uid']));
}
}
}
/**
* @brief Clear cache entries
*
* @param App $a
*/
function cron_clear_cache(App $a) {
$last = get_config('system','cache_last_cleared');
if ($last) {
$next = $last + (3600); // Once per hour
$clear_cache = ($next <= time());
} else {
$clear_cache = true;
}
if (!$clear_cache) {
return;
}
// clear old cache
Cache::clear();
// clear old item cache files
clear_cache();
// clear cache for photos
clear_cache($a->get_basepath(), $a->get_basepath()."/photo");
// clear smarty cache
clear_cache($a->get_basepath()."/view/smarty3/compiled", $a->get_basepath()."/view/smarty3/compiled");
// clear cache for image proxy
if (!get_config("system", "proxy_disabled")) {
clear_cache($a->get_basepath(), $a->get_basepath()."/proxy");
$cachetime = get_config('system','proxy_cache_time');
if (!$cachetime) {
$cachetime = PROXY_DEFAULT_TIME;
}
q('DELETE FROM `photo` WHERE `uid` = 0 AND `resource-id` LIKE "pic:%%" AND `created` < NOW() - INTERVAL %d SECOND', $cachetime);
}
// Delete the cached OEmbed entries that are older than one year
q("DELETE FROM `oembed` WHERE `created` < NOW() - INTERVAL 3 MONTH");
// Delete the cached "parse_url" entries that are older than one year
q("DELETE FROM `parsed_url` WHERE `created` < NOW() - INTERVAL 3 MONTH");
// Maximum table size in megabyte
$max_tablesize = intval(get_config('system','optimize_max_tablesize')) * 1000000;
if ($max_tablesize == 0) {
$max_tablesize = 100 * 1000000; // Default are 100 MB
}
if ($max_tablesize > 0) {
// Minimum fragmentation level in percent
$fragmentation_level = intval(get_config('system','optimize_fragmentation')) / 100;
if ($fragmentation_level == 0) {
$fragmentation_level = 0.3; // Default value is 30%
}
// Optimize some tables that need to be optimized
$r = q("SHOW TABLE STATUS");
foreach ($r as $table) {
// Don't optimize tables that are too large
if ($table["Data_length"] > $max_tablesize) {
continue;
}
// Don't optimize empty tables
if ($table["Data_length"] == 0) {
continue;
}
// Calculate fragmentation
$fragmentation = $table["Data_free"] / ($table["Data_length"] + $table["Index_length"]);
logger("Table ".$table["Name"]." - Fragmentation level: ".round($fragmentation * 100, 2), LOGGER_DEBUG);
// Don't optimize tables that needn't to be optimized
if ($fragmentation < $fragmentation_level) {
continue;
}
// So optimize it
logger("Optimize Table ".$table["Name"], LOGGER_DEBUG);
q("OPTIMIZE TABLE `%s`", dbesc($table["Name"]));
}
}
set_config('system','cache_last_cleared', time());
}
/**
* @brief Repair missing values in Diaspora contacts
*
* @param App $a
*/
function cron_repair_diaspora(App $a) {
$starttime = time();
$r = q("SELECT `id`, `url` FROM `contact`
WHERE `network` = '%s' AND (`batch` = '' OR `notify` = '' OR `poll` = '' OR pubkey = '')
ORDER BY RAND() LIMIT 50", dbesc(NETWORK_DIASPORA));
if (!dbm::is_result($r)) {
return;
}
foreach ($r AS $contact) {
// Quit the loop after 3 minutes
if (time() > ($starttime + 180)) {
return;
}
if (!poco_reachable($contact["url"])) {
continue;
}
$data = Probe::uri($contact["url"]);
if ($data["network"] != NETWORK_DIASPORA) {
continue;
}
logger("Repair contact ".$contact["id"]." ".$contact["url"], LOGGER_DEBUG);
q("UPDATE `contact` SET `batch` = '%s', `notify` = '%s', `poll` = '%s', pubkey = '%s' WHERE `id` = %d",
dbesc($data["batch"]), dbesc($data["notify"]), dbesc($data["poll"]), dbesc($data["pubkey"]),
intval($contact["id"]));
}
}
/**
* @brief Do some repairs in database entries
*
*/
function cron_repair_database() {
// Sometimes there seem to be issues where the "self" contact vanishes.
// We haven't found the origin of the problem by now.
$r = q("SELECT `uid` FROM `user` WHERE NOT EXISTS (SELECT `uid` FROM `contact` WHERE `contact`.`uid` = `user`.`uid` AND `contact`.`self`)");
if (dbm::is_result($r)) {
foreach ($r AS $user) {
logger('Create missing self contact for user '.$user['uid']);
user_create_self_contact($user['uid']);
}
}
// Set the parent if it wasn't set. (Shouldn't happen - but does sometimes)
// This call is very "cheap" so we can do it at any time without a problem
q("UPDATE `item` INNER JOIN `item` AS `parent` ON `parent`.`uri` = `item`.`parent-uri` AND `parent`.`uid` = `item`.`uid` SET `item`.`parent` = `parent`.`id` WHERE `item`.`parent` = 0");
// There was an issue where the nick vanishes from the contact table
q("UPDATE `contact` INNER JOIN `user` ON `contact`.`uid` = `user`.`uid` SET `nick` = `nickname` WHERE `self` AND `nick`=''");
// Update the global contacts for local users
$r = q("SELECT `uid` FROM `user` WHERE `verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired`");
if (dbm::is_result($r)) {
foreach ($r AS $user) {
update_gcontact_for_user($user["uid"]);
}
}
/// @todo
/// - remove thread entries without item
/// - remove sign entries without item
/// - remove children when parent got lost
/// - set contact-id in item when not present
}

View file

@ -1,94 +1,52 @@
<?php
require_once('library/ASNValue.class.php');
require_once('library/asn1.php');
require_once 'library/ASNValue.class.php';
require_once 'library/asn1.php';
// supported algorithms are 'sha256', 'sha1'
function rsa_sign($data,$key,$alg = 'sha256') {
$sig = '';
if (version_compare(PHP_VERSION, '5.3.0', '>=') || $alg === 'sha1') {
openssl_sign($data,$sig,$key,(($alg == 'sha1') ? OPENSSL_ALGO_SHA1 : $alg));
}
else {
if(strlen($key) < 1024 || extension_loaded('gmp')) {
require_once('library/phpsec/Crypt/RSA.php');
$rsa = new CRYPT_RSA();
$rsa->signatureMode = CRYPT_RSA_SIGNATURE_PKCS1;
$rsa->setHash($alg);
$rsa->loadKey($key);
$sig = $rsa->sign($data);
}
else {
logger('rsa_sign: insecure algorithm used. Please upgrade PHP to 5.3');
openssl_private_encrypt(hex2bin('3031300d060960864801650304020105000420') . hash('sha256',$data,true), $sig, $key);
}
}
function rsa_sign($data, $key, $alg = 'sha256') {
openssl_sign($data, $sig, $key, (($alg == 'sha1') ? OPENSSL_ALGO_SHA1 : $alg));
return $sig;
}
function rsa_verify($data,$sig,$key,$alg = 'sha256') {
if (version_compare(PHP_VERSION, '5.3.0', '>=') || $alg === 'sha1') {
$verify = openssl_verify($data,$sig,$key,(($alg == 'sha1') ? OPENSSL_ALGO_SHA1 : $alg));
}
else {
if(strlen($key) <= 300 || extension_loaded('gmp')) {
require_once('library/phpsec/Crypt/RSA.php');
$rsa = new CRYPT_RSA();
$rsa->signatureMode = CRYPT_RSA_SIGNATURE_PKCS1;
$rsa->setHash($alg);
$rsa->loadKey($key);
$verify = $rsa->verify($data,$sig);
}
else {
// fallback sha256 verify for PHP < 5.3 and large key lengths
$rawsig = '';
openssl_public_decrypt($sig,$rawsig,$key);
$verify = (($rawsig && substr($rawsig,-32) === hash('sha256',$data,true)) ? true : false);
}
}
return $verify;
function rsa_verify($data, $sig, $key, $alg = 'sha256') {
return openssl_verify($data, $sig, $key, (($alg == 'sha1') ? OPENSSL_ALGO_SHA1 : $alg));
}
function DerToPem($Der, $Private = false) {
//Encode:
$Der = base64_encode($Der);
//Split lines:
$lines = str_split($Der, 65);
$body = implode("\n", $lines);
//Get title:
$title = $Private ? 'RSA PRIVATE KEY' : 'PUBLIC KEY';
//Add wrapping:
$result = "-----BEGIN {$title}-----\n";
$result .= $body . "\n";
$result .= "-----END {$title}-----\n";
function DerToPem($Der, $Private=false)
{
//Encode:
$Der = base64_encode($Der);
//Split lines:
$lines = str_split($Der, 65);
$body = implode("\n", $lines);
//Get title:
$title = $Private? 'RSA PRIVATE KEY' : 'PUBLIC KEY';
//Add wrapping:
$result = "-----BEGIN {$title}-----\n";
$result .= $body . "\n";
$result .= "-----END {$title}-----\n";
return $result;
return $result;
}
function DerToRsa($Der)
{
//Encode:
$Der = base64_encode($Der);
//Split lines:
$lines = str_split($Der, 64);
$body = implode("\n", $lines);
//Get title:
$title = 'RSA PUBLIC KEY';
//Add wrapping:
$result = "-----BEGIN {$title}-----\n";
$result .= $body . "\n";
$result .= "-----END {$title}-----\n";
return $result;
function DerToRsa($Der) {
//Encode:
$Der = base64_encode($Der);
//Split lines:
$lines = str_split($Der, 64);
$body = implode("\n", $lines);
//Get title:
$title = 'RSA PUBLIC KEY';
//Add wrapping:
$result = "-----BEGIN {$title}-----\n";
$result .= $body . "\n";
$result .= "-----END {$title}-----\n";
return $result;
}
function pkcs8_encode($Modulus,$PublicExponent) {
function pkcs8_encode($Modulus, $PublicExponent) {
//Encode key sequence
$modulus = new ASNValue(ASNValue::TAG_INTEGER);
$modulus->SetIntBuffer($Modulus);
@ -111,8 +69,7 @@ function pkcs8_encode($Modulus,$PublicExponent) {
return $PublicDER;
}
function pkcs1_encode($Modulus,$PublicExponent) {
function pkcs1_encode($Modulus, $PublicExponent) {
//Encode key sequence
$modulus = new ASNValue(ASNValue::TAG_INTEGER);
$modulus->SetIntBuffer($Modulus);
@ -126,22 +83,20 @@ function pkcs1_encode($Modulus,$PublicExponent) {
return $bitStringValue;
}
function metopem($m,$e) {
$der = pkcs8_encode($m,$e);
$key = DerToPem($der,false);
function metopem($m, $e) {
$der = pkcs8_encode($m, $e);
$key = DerToPem($der, false);
return $key;
}
}
function pubrsatome($key,&$m,&$e) {
require_once('library/asn1.php');
require_once('include/salmon.php');
$lines = explode("\n",$key);
$lines = explode("\n", $key);
unset($lines[0]);
unset($lines[count($lines)]);
$x = base64_decode(implode('',$lines));
$x = base64_decode(implode('', $lines));
$r = ASN_BASE::parseASNString($x);
@ -151,21 +106,21 @@ function pubrsatome($key,&$m,&$e) {
function rsatopem($key) {
pubrsatome($key,$m,$e);
return(metopem($m,$e));
pubrsatome($key, $m, $e);
return metopem($m, $e);
}
function pemtorsa($key) {
pemtome($key,$m,$e);
return(metorsa($m,$e));
pemtome($key, $m, $e);
return metorsa($m, $e);
}
function pemtome($key,&$m,&$e) {
function pemtome($key, &$m, &$e) {
require_once('include/salmon.php');
$lines = explode("\n",$key);
$lines = explode("\n", $key);
unset($lines[0]);
unset($lines[count($lines)]);
$x = base64_decode(implode('',$lines));
$x = base64_decode(implode('', $lines));
$r = ASN_BASE::parseASNString($x);
@ -173,82 +128,36 @@ function pemtome($key,&$m,&$e) {
$e = base64url_decode($r[0]->asnData[1]->asnData[0]->asnData[1]->asnData);
}
function metorsa($m,$e) {
$der = pkcs1_encode($m,$e);
function metorsa($m, $e) {
$der = pkcs1_encode($m, $e);
$key = DerToRsa($der);
return $key;
}
}
function salmon_key($pubkey) {
pemtome($pubkey,$m,$e);
return 'RSA' . '.' . base64url_encode($m,true) . '.' . base64url_encode($e,true) ;
pemtome($pubkey, $m, $e);
return 'RSA' . '.' . base64url_encode($m, true) . '.' . base64url_encode($e, true) ;
}
if(! function_exists('aes_decrypt')) {
// DEPRECATED IN 3.4.1
function aes_decrypt($val,$ky)
{
$key="\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
for($a=0;$a<strlen($ky);$a++)
$key[$a%16]=chr(ord($key[$a%16]) ^ ord($ky[$a]));
$mode = MCRYPT_MODE_ECB;
$enc = MCRYPT_RIJNDAEL_128;
$dec = @mcrypt_decrypt($enc, $key, $val, $mode, @mcrypt_create_iv( @mcrypt_get_iv_size($enc, $mode), MCRYPT_DEV_URANDOM ) );
return rtrim($dec,(( ord(substr($dec,strlen($dec)-1,1))>=0 and ord(substr($dec, strlen($dec)-1,1))<=16)? chr(ord( substr($dec,strlen($dec)-1,1))):null));
}}
if(! function_exists('aes_encrypt')) {
// DEPRECATED IN 3.4.1
function aes_encrypt($val,$ky)
{
$key="\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
for($a=0;$a<strlen($ky);$a++)
$key[$a%16]=chr(ord($key[$a%16]) ^ ord($ky[$a]));
$mode=MCRYPT_MODE_ECB;
$enc=MCRYPT_RIJNDAEL_128;
$val=str_pad($val, (16*(floor(strlen($val) / 16)+(strlen($val) % 16==0?2:1))), chr(16-(strlen($val) % 16)));
return mcrypt_encrypt($enc, $key, $val, $mode, mcrypt_create_iv( mcrypt_get_iv_size($enc, $mode), MCRYPT_DEV_URANDOM));
}}
function pkcs5_pad ($text, $blocksize)
{
$pad = $blocksize - (strlen($text) % $blocksize);
return $text . str_repeat(chr($pad), $pad);
}
function pkcs5_unpad($text)
{
$pad = ord($text{strlen($text)-1});
if ($pad > strlen($text)) return false;
if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) return false;
return substr($text, 0, -1 * $pad);
}
function new_keypair($bits) {
$openssl_options = array(
'digest_alg' => 'sha1',
'private_key_bits' => $bits,
'encrypt_key' => false
'encrypt_key' => false
);
$conf = get_config('system','openssl_conf_file');
if($conf)
$conf = get_config('system', 'openssl_conf_file');
if ($conf) {
$openssl_options['config'] = $conf;
}
$result = openssl_pkey_new($openssl_options);
if(empty($result)) {
if (empty($result)) {
logger('new_keypair: failed');
return false;
}
// Get private key
$response = array('prvkey' => '', 'pubkey' => '');
openssl_pkey_export($result, $response['prvkey']);
@ -258,6 +167,4 @@ function new_keypair($bits) {
$response['pubkey'] = $pkey["key"];
return $response;
}

View file

@ -931,6 +931,30 @@ class dfrn {
return $entry;
}
/**
* @brief encrypts data via AES
*
* @param string $data The data that is to be encrypted
* @param string $key The AES key
*
* @return string encrypted data
*/
private static function aes_encrypt($data, $key) {
return openssl_encrypt($data, 'aes-128-ecb', $key, OPENSSL_RAW_DATA);
}
/**
* @brief decrypts data via AES
*
* @param string $encrypted The encrypted data
* @param string $key The AES key
*
* @return string decrypted data
*/
public static function aes_decrypt($encrypted, $key) {
return openssl_decrypt($encrypted, 'aes-128-ecb', $key, OPENSSL_RAW_DATA);
}
/**
* @brief Delivers the atom content to the contacts
*
@ -958,11 +982,6 @@ class dfrn {
$rino = get_config('system', 'rino_encrypt');
$rino = intval($rino);
// use RINO1 if mcrypt isn't installed and RINO2 was selected
if ($rino == 2 and !function_exists('mcrypt_create_iv')) {
$rino = 1;
}
logger("Local rino version: ". $rino, LOGGER_DEBUG);
$ssl_val = intval(get_config('system','ssl_policy'));
@ -1093,8 +1112,8 @@ class dfrn {
switch ($rino_remote_version) {
case 1:
// Deprecated rino version!
$key = substr(random_string(), 0, 16);
$data = aes_encrypt($postvars['data'],$key);
$key = openssl_random_pseudo_bytes(16);
$data = self::aes_encrypt($postvars['data'], $key);
break;
case 2:
// RINO 2 based on php-encryption
@ -1458,7 +1477,9 @@ class dfrn {
$poco["photo"] = $author["avatar"];
$poco["hide"] = $hide;
$poco["contact-type"] = $contact["contact-type"];
update_gcontact($poco);
$gcid = update_gcontact($poco);
link_gcontact($gcid, $importer["uid"], $contact["id"]);
}
return($author);

View file

@ -10,17 +10,17 @@
use \Friendica\Core\Config;
require_once("include/items.php");
require_once("include/bb2diaspora.php");
require_once("include/Scrape.php");
require_once("include/Contact.php");
require_once("include/Photo.php");
require_once("include/socgraph.php");
require_once("include/group.php");
require_once("include/xml.php");
require_once("include/datetime.php");
require_once("include/queue_fn.php");
require_once("include/cache.php");
require_once 'include/items.php';
require_once 'include/bb2diaspora.php';
require_once 'include/Scrape.php';
require_once 'include/Contact.php';
require_once 'include/Photo.php';
require_once 'include/socgraph.php';
require_once 'include/group.php';
require_once 'include/xml.php';
require_once 'include/datetime.php';
require_once 'include/queue_fn.php';
require_once 'include/cache.php';
/**
* @brief This class contain functions to create and send Diaspora XML files
@ -160,6 +160,32 @@ class Diaspora {
return $data;
}
/**
* @brief encrypts data via AES
*
* @param string $key The AES key
* @param string $iv The IV (is used for CBC encoding)
* @param string $data The data that is to be encrypted
*
* @return string encrypted data
*/
private static function aes_encrypt($key, $iv, $data) {
return openssl_encrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
}
/**
* @brief decrypts data via AES
*
* @param string $key The AES key
* @param string $iv The IV (is used for CBC encoding)
* @param string $encrypted The encrypted data
*
* @return string decrypted data
*/
private static function aes_decrypt($key, $iv, $encrypted) {
return openssl_decrypt($encrypted,'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA,str_pad($iv, 16, "\0"));
}
/**
* @brief: Decodes incoming Diaspora message
*
@ -199,10 +225,7 @@ class Diaspora {
$outer_iv = base64_decode($j_outer_key_bundle->iv);
$outer_key = base64_decode($j_outer_key_bundle->key);
$decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $outer_key, $ciphertext, MCRYPT_MODE_CBC, $outer_iv);
$decrypted = pkcs5_unpad($decrypted);
$decrypted = self::aes_decrypt($outer_key, $outer_iv, $ciphertext);
logger('decrypted: '.$decrypted, LOGGER_DEBUG);
$idom = parse_xml_string($decrypted,false);
@ -261,8 +284,7 @@ class Diaspora {
// Decode the encrypted blob
$inner_encrypted = base64_decode($data);
$inner_decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $inner_aes_key, $inner_encrypted, MCRYPT_MODE_CBC, $inner_iv);
$inner_decrypted = pkcs5_unpad($inner_decrypted);
$inner_decrypted = self::aes_decrypt($inner_aes_key, $inner_iv, $inner_encrypted);
}
if (!$author_link) {
@ -1848,18 +1870,15 @@ class Diaspora {
intval($importer["uid"])
);
if ($searchable) {
poco_check($contact["url"], $name, NETWORK_DIASPORA, $image_url, $about, $location, $gender, $keywords, "",
datetime_convert(), 2, $contact["id"], $importer["uid"]);
}
$gcontact = array("url" => $contact["url"], "network" => NETWORK_DIASPORA, "generation" => 2,
"photo" => $image_url, "name" => $name, "location" => $location,
"about" => $about, "birthday" => $birthday, "gender" => $gender,
"addr" => $author, "nick" => $nick, "keywords" => $keywords,
"hide" => !$searchable, "nsfw" => $nsfw);
update_gcontact($gcontact);
$gcid = update_gcontact($gcontact);
link_gcontact($gcid, $importer["uid"], $contact["id"]);
logger("Profile of contact ".$contact["id"]." stored for user ".$importer["uid"], LOGGER_DEBUG);
@ -2621,20 +2640,19 @@ class Diaspora {
return false;
}
$inner_aes_key = random_string(32);
$inner_aes_key = openssl_random_pseudo_bytes(32);
$b_inner_aes_key = base64_encode($inner_aes_key);
$inner_iv = random_string(16);
$inner_iv = openssl_random_pseudo_bytes(16);
$b_inner_iv = base64_encode($inner_iv);
$outer_aes_key = random_string(32);
$outer_aes_key = openssl_random_pseudo_bytes(32);
$b_outer_aes_key = base64_encode($outer_aes_key);
$outer_iv = random_string(16);
$outer_iv = openssl_random_pseudo_bytes(16);
$b_outer_iv = base64_encode($outer_iv);
$handle = self::my_handle($user);
$padded_data = pkcs5_pad($msg,16);
$inner_encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $inner_aes_key, $padded_data, MCRYPT_MODE_CBC, $inner_iv);
$inner_encrypted = self::aes_encrypt($inner_aes_key, $inner_iv, $msg);
$b64_data = base64_encode($inner_encrypted);
@ -2656,9 +2674,8 @@ class Diaspora {
"author_id" => $handle));
$decrypted_header = xml::from_array($xmldata, $xml, true);
$decrypted_header = pkcs5_pad($decrypted_header,16);
$ciphertext = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $outer_aes_key, $decrypted_header, MCRYPT_MODE_CBC, $outer_iv);
$ciphertext = self::aes_encrypt($outer_aes_key, $outer_iv, $decrypted_header);
$outer_json = json_encode(array("iv" => $b_outer_iv, "key" => $b_outer_aes_key));

View file

@ -16,6 +16,7 @@ function discover_poco_run(&$argv, &$argc) {
- update_server: Frequently check the first 250 servers for vitality.
- update_server_directory: Discover the given server id for their contacts
- poco_load: Load POCO data from a given POCO address
- check_profile: Update remote profile data
*/
if (($argc > 2) && ($argv[1] == "dirsearch")) {
@ -33,6 +34,8 @@ function discover_poco_run(&$argv, &$argc) {
$mode = 6;
} elseif (($argc > 5) && ($argv[1] == "poco_load")) {
$mode = 7;
} elseif (($argc == 3) && ($argv[1] == "check_profile")) {
$mode = 8;
} elseif ($argc == 1) {
$search = "";
$mode = 0;
@ -42,7 +45,12 @@ function discover_poco_run(&$argv, &$argc) {
logger('start '.$search);
if ($mode == 7) {
if ($mode == 8) {
$profile_url = base64_decode($argv[2]);
if ($profile_url != "") {
poco_last_updated($profile_url, true);
}
} elseif ($mode == 7) {
if ($argc == 6) {
$url = base64_decode($argv[5]);
} else {
@ -121,7 +129,9 @@ function update_server() {
function discover_users() {
logger("Discover users", LOGGER_DEBUG);
$users = q("SELECT `url`, `created`, `updated`, `last_failure`, `last_contact`, `server_url` FROM `gcontact`
$starttime = time();
$users = q("SELECT `url`, `created`, `updated`, `last_failure`, `last_contact`, `server_url`, `network` FROM `gcontact`
WHERE `last_contact` < UTC_TIMESTAMP - INTERVAL 1 MONTH AND
`last_failure` < UTC_TIMESTAMP - INTERVAL 1 MONTH AND
`network` IN ('%s', '%s', '%s', '%s', '') ORDER BY rand()",
@ -155,14 +165,19 @@ function discover_users() {
continue;
}
$server_url = poco_detect_server($user["url"]);
$force_update = false;
if ($user["server_url"] != "") {
$force_update = (normalise_link($user["server_url"]) != normalise_link($server_url));
$server_url = $user["server_url"];
} else {
$server_url = poco_detect_server($user["url"]);
}
if (($server_url == "") OR poco_check_server($server_url, $gcontacts[0]["network"])) {
logger('Check user '.$user["url"]);
poco_last_updated($user["url"], true);
if ((($server_url == "") AND ($user["network"] == NETWORK_FEED)) OR $force_update OR poco_check_server($server_url, $user["network"])) {
logger('Check profile '.$user["url"]);
proc_run(PRIORITY_LOW, "include/discover_poco.php", "check_profile", base64_encode($user["url"]));
if (++$checked > 100) {
return;
@ -171,6 +186,11 @@ function discover_users() {
q("UPDATE `gcontact` SET `last_failure` = '%s' WHERE `nurl` = '%s'",
dbesc(datetime_convert()), dbesc(normalise_link($user["url"])));
}
// Quit the loop after 3 minutes
if (time() > ($starttime + 180)) {
return;
}
}
}
@ -217,7 +237,14 @@ function discover_directory($search) {
if ($data["network"] == NETWORK_DFRN) {
logger("Profile ".$jj->url." is reachable (".$search.")", LOGGER_DEBUG);
logger("Add profile ".$jj->url." to local directory (".$search.")", LOGGER_DEBUG);
poco_check($data["url"], $data["name"], $data["network"], $data["photo"], "", "", "", $jj->tags, $data["addr"], "", 0);
if ($jj->tags != "") {
$data["keywords"] = $jj->tags;
}
$data["server_url"] = $data["baseurl"];
update_gcontact($data);
} else {
logger("Profile ".$jj->url." is not responding or no Friendica contact - but network ".$data["network"], LOGGER_DEBUG);
}

View file

@ -2,7 +2,7 @@
/**
* @file include/html2bbcode.php
* @brief Converter for HTML to BBCode
*
*
* Made by: ike@piratenpartei.de
* Originally made for the syncom project: http://wiki.piratenpartei.de/Syncom
* https://github.com/annando/Syncom
@ -79,16 +79,25 @@ function node2bbcodesub(&$doc, $oldnode, $attributes, $startbb, $endbb)
return($replace);
}
function _replace_code_cb($m){
return "<code>".str_replace("\n","<br>\n",$m[1]). "</code>";
}
function html2bbcode($message)
{
$message = str_replace("\r", "", $message);
$message = preg_replace_callback("|<pre><code>([^<]*)</code></pre>|ism", "_replace_code_cb", $message);
// Removing code blocks before the whitespace removal processing below
$codeblocks = [];
$message = preg_replace_callback('#<pre><code(?: class="([^"]*)")?>(.*)</code></pre>#iUs',
function ($matches) use (&$codeblocks) {
$return = '[codeblock-' . count($codeblocks) . ']';
$prefix = '[code]';
if ($matches[1] != '') {
$prefix = '[code=' . $matches[1] . ']';
}
$codeblocks[] = $prefix . $matches[2] . '[/code]';
return $return;
}
, $message);
$message = str_replace(array(
"<li><p>",
@ -232,7 +241,6 @@ function html2bbcode($message)
node2bbcode($doc, 'audio', array('src'=>'/(.+)/'), '[audio]$1', '[/audio]');
node2bbcode($doc, 'iframe', array('src'=>'/(.+)/'), '[iframe]$1', '[/iframe]');
node2bbcode($doc, 'code', array(), '[code]', '[/code]');
node2bbcode($doc, 'key', array(), '[code]', '[/code]');
$message = $doc->saveHTML();
@ -302,6 +310,19 @@ function html2bbcode($message)
// Handling Yahoo style of mails
$message = str_replace('[hr][b]From:[/b]', '[quote][b]From:[/b]', $message);
return(trim($message));
// Restore code blocks
$message = preg_replace_callback('#\[codeblock-([0-9]+)\]#iU',
function ($matches) use ($codeblocks) {
$return = '';
if (isset($codeblocks[intval($matches[1])])) {
$return = $codeblocks[$matches[1]];
}
return $return;
}
, $message);
$message = trim($message);
return $message;
}
?>

View file

@ -17,10 +17,10 @@ function oembed_replacecb($matches){
/**
* @brief Get data from an URL to embed its content.
*
*
* @param string $embedurl The URL from which the data should be fetched.
* @param bool $no_rich_type If set to true rich type content won't be fetched.
*
*
* @return bool|object Returns object with embed content or false if no embedable
* content exists
*/
@ -41,8 +41,8 @@ function oembed_fetch_url($embedurl, $no_rich_type = false){
// These media files should now be caught in bbcode.php
// left here as a fallback in case this is called from another source
$noexts = array("mp3","mp4","ogg","ogv","oga","ogm","webm");
$ext = pathinfo(strtolower($embedurl),PATHINFO_EXTENSION);
$noexts = array("mp3", "mp4", "ogg", "ogv", "oga", "ogm", "webm");
$ext = pathinfo(strtolower($embedurl), PATHINFO_EXTENSION);
if (is_null($txt)) {
@ -74,21 +74,10 @@ function oembed_fetch_url($embedurl, $no_rich_type = false){
}
}
if ($txt==false || $txt=="") {
$embedly = Config::get("system", "embedly");
if ($embedly != "") {
// try embedly service
$ourl = "https://api.embed.ly/1/oembed?key=".$embedly."&url=".urlencode($embedurl);
$txt = fetch_url($ourl);
$txt = trim($txt);
logger("oembed_fetch_url: ".$txt, LOGGER_DEBUG);
}
}
$txt=trim($txt);
if ($txt[0]!="{") {
$txt='{"type":"error"}';
if ($txt[0] != "{") {
$txt = '{"type":"error"}';
} else { //save in cache
$j = json_decode($txt);
if ($j->type != "error") {

View file

@ -132,9 +132,6 @@ class ostatus {
dbesc($contact["name"]), dbesc($contact["nick"]), dbesc($contact["alias"]),
dbesc($contact["about"]), dbesc($contact["location"]),
dbesc(datetime_convert()), intval($contact["id"]));
poco_check($contact["url"], $contact["name"], $contact["network"], $author["author-avatar"], $contact["about"], $contact["location"],
"", "", "", datetime_convert(), 2, $contact["id"], $contact["uid"]);
}
if (isset($author["author-avatar"]) AND ($author["author-avatar"] != $r[0]['avatar'])) {
@ -163,7 +160,9 @@ class ostatus {
$contact["generation"] = 2;
$contact["hide"] = false; // OStatus contacts are never hidden
$contact["photo"] = $author["author-avatar"];
update_gcontact($contact);
$gcid = update_gcontact($contact);
link_gcontact($gcid, $contact["uid"], $contact["id"]);
}
return($author);
@ -808,6 +807,9 @@ class ostatus {
/// @todo This function is totally ugly and has to be rewritten totally
// Import all threads or only threads that were started by our followers?
$all_threads = !get_config('system','ostatus_full_threads');
$item_stored = -1;
$conversation_url = self::fetch_conversation($self, $conversation_url);
@ -816,8 +818,8 @@ class ostatus {
// Don't do a completion on liked content
if (((intval(get_config('system','ostatus_poll_interval')) == -2) AND (count($item) > 0)) OR
($item["verb"] == ACTIVITY_LIKE) OR ($conversation_url == "")) {
$item_stored = item_store($item, true);
return($item_stored);
$item_stored = item_store($item, $all_threads);
return $item_stored;
}
// Get the parent
@ -897,7 +899,7 @@ class ostatus {
if (!sizeof($items)) {
if (count($item) > 0) {
$item_stored = item_store($item, true);
$item_stored = item_store($item, $all_threads);
if ($item_stored) {
logger("Conversation ".$conversation_url." couldn't be fetched. Item uri ".$item["uri"]." stored: ".$item_stored, LOGGER_DEBUG);
@ -1195,7 +1197,7 @@ class ostatus {
}
}
$item_stored = item_store($item, true);
$item_stored = item_store($item, $all_threads);
if ($item_stored) {
logger("Uri ".$item["uri"]." wasn't found in conversation ".$conversation_url, LOGGER_DEBUG);
self::store_conversation($item_stored, $conversation_url);

View file

@ -34,7 +34,7 @@ require_once("include/Photo.php");
*/
function poco_load($cid, $uid = 0, $zcid = 0, $url = null) {
// Call the function "poco_load_worker" via the worker
proc_run(PRIORITY_LOW, "include/discover_poco.php", "poco_load", $cid, $uid, $zcid, base64_encode($url));
proc_run(PRIORITY_LOW, "include/discover_poco.php", "poco_load", intval($cid), intval($uid), intval($zcid), base64_encode($url));
}
/**
@ -159,27 +159,27 @@ function poco_load_worker($cid, $uid, $zcid, $url) {
if (isset($entry->contactType) AND ($entry->contactType >= 0))
$contact_type = $entry->contactType;
// If you query a Friendica server for its profiles, the network has to be Friendica
/// TODO It could also be a Redmatrix server
//if ($uid == 0)
// $network = NETWORK_DFRN;
$gcontact = array("url" => $profile_url,
"name" => $name,
"network" => $network,
"photo" => $profile_photo,
"about" => $about,
"location" => $location,
"gender" => $gender,
"keywords" => $keywords,
"connect" => $connect_url,
"updated" => $updated,
"contact-type" => $contact_type,
"generation" => $generation);
poco_check($profile_url, $name, $network, $profile_photo, $about, $location, $gender, $keywords, $connect_url, $updated, $generation, $cid, $uid, $zcid);
try {
$gcontact = sanitize_gcontact($gcontact);
$gcid = update_gcontact($gcontact);
$gcontact = array("url" => $profile_url, "contact-type" => $contact_type, "generation" => $generation);
update_gcontact($gcontact);
// Update the Friendica contacts. Diaspora is doing it via a message. (See include/diaspora.php)
// Deactivated because we now update Friendica contacts in dfrn.php
//if (($location != "") OR ($about != "") OR ($keywords != "") OR ($gender != ""))
// q("UPDATE `contact` SET `location` = '%s', `about` = '%s', `keywords` = '%s', `gender` = '%s'
// WHERE `nurl` = '%s' AND NOT `self` AND `network` = '%s'",
// dbesc($location),
// dbesc($about),
// dbesc($keywords),
// dbesc($gender),
// dbesc(normalise_link($profile_url)),
// dbesc(NETWORK_DFRN));
link_gcontact($gcid, $uid, $cid, $zcid);
} catch (Exception $e) {
logger($e->getMessage(), LOGGER_DEBUG);
}
}
logger("poco_load: loaded $total entries",LOGGER_DEBUG);
@ -190,172 +190,158 @@ function poco_load_worker($cid, $uid, $zcid, $url) {
);
}
/**
* @brief Sanitize the given gcontact data
*
* @param array $gcontact array with gcontact data
* @throw Exception
*
* Generation:
* 0: No definition
* 1: Profiles on this server
* 2: Contacts of profiles on this server
* 3: Contacts of contacts of profiles on this server
* 4: ...
*
*/
function sanitize_gcontact($gcontact) {
function poco_check($profile_url, $name, $network, $profile_photo, $about, $location, $gender, $keywords, $connect_url, $updated, $generation, $cid = 0, $uid = 0, $zcid = 0) {
if ($gcontact['url'] == "") {
throw new Exception('URL is empty');
}
// Generation:
// 0: No definition
// 1: Profiles on this server
// 2: Contacts of profiles on this server
// 3: Contacts of contacts of profiles on this server
// 4: ...
$gcid = "";
if ($profile_url == "")
return $gcid;
$urlparts = parse_url($profile_url);
if (!isset($urlparts["scheme"]))
return $gcid;
$urlparts = parse_url($gcontact['url']);
if (!isset($urlparts["scheme"])) {
throw new Exception("This (".$gcontact['url'].") doesn't seem to be an url.");
}
if (in_array($urlparts["host"], array("www.facebook.com", "facebook.com", "twitter.com",
"identi.ca", "alpha.app.net")))
return $gcid;
"identi.ca", "alpha.app.net"))) {
throw new Exception('Contact from a non federated network ignored. ('.$gcontact['url'].')');
}
// Don't store the statusnet connector as network
// We can't simply set this to NETWORK_OSTATUS since the connector could have fetched posts from friendica as well
if ($network == NETWORK_STATUSNET)
$network = "";
if ($gcontact['network'] == NETWORK_STATUSNET) {
$gcontact['network'] = "";
}
// Assure that there are no parameter fragments in the profile url
if (in_array($network, array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, "")))
$profile_url = clean_contact_url($profile_url);
if (in_array($gcontact['network'], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, ""))) {
$gcontact['url'] = clean_contact_url($gcontact['url']);
}
$alternate = poco_alternate_ostatus_url($profile_url);
$orig_updated = $updated;
$alternate = poco_alternate_ostatus_url($gcontact['url']);
// The global contacts should contain the original picture, not the cached one
if (($generation != 1) AND stristr(normalise_link($profile_photo), normalise_link(App::get_baseurl()."/photo/"))) {
$profile_photo = "";
if (($gcontact['generation'] != 1) AND stristr(normalise_link($gcontact['photo']), normalise_link(App::get_baseurl()."/photo/"))) {
$gcontact['photo'] = "";
}
$r = q("SELECT `network` FROM `contact` WHERE `nurl` = '%s' AND `network` != '' AND `network` != '%s' LIMIT 1",
dbesc(normalise_link($profile_url)), dbesc(NETWORK_STATUSNET)
);
if (dbm::is_result($r)) {
$network = $r[0]["network"];
}
if (($network == "") OR ($network == NETWORK_OSTATUS)) {
$r = q("SELECT `network`, `url` FROM `contact` WHERE `alias` IN ('%s', '%s') AND `network` != '' AND `network` != '%s' LIMIT 1",
dbesc($profile_url), dbesc(normalise_link($profile_url)), dbesc(NETWORK_STATUSNET)
if (!isset($gcontact['network'])) {
$r = q("SELECT `network` FROM `contact` WHERE `uid` = 0 AND `nurl` = '%s' AND `network` != '' AND `network` != '%s' LIMIT 1",
dbesc(normalise_link($gcontact['url'])), dbesc(NETWORK_STATUSNET)
);
if (dbm::is_result($r)) {
$network = $r[0]["network"];
//$profile_url = $r[0]["url"];
$gcontact['network'] = $r[0]["network"];
}
if (($gcontact['network'] == "") OR ($gcontact['network'] == NETWORK_OSTATUS)) {
$r = q("SELECT `network`, `url` FROM `contact` WHERE `uid` = 0 AND `alias` IN ('%s', '%s') AND `network` != '' AND `network` != '%s' LIMIT 1",
dbesc($gcontact['url']), dbesc(normalise_link($gcontact['url'])), dbesc(NETWORK_STATUSNET)
);
if (dbm::is_result($r)) {
$gcontact['network'] = $r[0]["network"];
}
}
}
$gcontact['server_url'] = '';
$gcontact['network'] = '';
$x = q("SELECT * FROM `gcontact` WHERE `nurl` = '%s' LIMIT 1",
dbesc(normalise_link($profile_url))
dbesc(normalise_link($gcontact['url']))
);
if (count($x)) {
if (($network == "") AND ($x[0]["network"] != NETWORK_STATUSNET)) {
$network = $x[0]["network"];
if (!isset($gcontact['network']) AND ($x[0]["network"] != NETWORK_STATUSNET)) {
$gcontact['network'] = $x[0]["network"];
}
if ($updated <= NULL_DATE) {
$updated = $x[0]["updated"];
if ($gcontact['updated'] <= NULL_DATE) {
$gcontact['updated'] = $x[0]["updated"];
}
if (!isset($gcontact['server_url']) AND (normalise_link($x[0]["server_url"]) != normalise_link($x[0]["url"]))) {
$gcontact['server_url'] = $x[0]["server_url"];
}
if (!isset($gcontact['addr'])) {
$gcontact['addr'] = $x[0]["addr"];
}
$created = $x[0]["created"];
$server_url = $x[0]["server_url"];
$nick = $x[0]["nick"];
$addr = $x[0]["addr"];
$alias = $x[0]["alias"];
$notify = $x[0]["notify"];
} else {
$created = NULL_DATE;
$server_url = "";
$urlparts = parse_url($profile_url);
$nick = end(explode("/", $urlparts["path"]));
$addr = "";
$alias = "";
$notify = "";
}
if ((($network == "") OR ($name == "") OR ($addr == "") OR ($profile_photo == "") OR ($server_url == "") OR $alternate)
AND poco_reachable($profile_url, $server_url, $network, false)) {
$data = probe_url($profile_url);
if ((!isset($gcontact['network']) OR !isset($gcontact['name']) OR !isset($gcontact['addr']) OR !isset($gcontact['photo']) OR !isset($gcontact['server_url']) OR $alternate)
AND poco_reachable($gcontact['url'], $gcontact['server_url'], $gcontact['network'], false)) {
$data = Probe::uri($gcontact['url']);
$orig_profile = $profile_url;
if ($data["network"] == NETWORK_PHANTOM) {
throw new Exception('Probing for URL '.$gcontact['url'].' failed');
}
$network = $data["network"];
$name = $data["name"];
$nick = $data["nick"];
$addr = $data["addr"];
$alias = $data["alias"];
$notify = $data["notify"];
$profile_url = $data["url"];
$profile_photo = $data["photo"];
$server_url = $data["baseurl"];
$orig_profile = $gcontact['url'];
if ($alternate AND ($network == NETWORK_OSTATUS)) {
$gcontact["server_url"] = $data["baseurl"];
$gcontact = array_merge($gcontact, $data);
if ($alternate AND ($gcontact['network'] == NETWORK_OSTATUS)) {
// Delete the old entry - if it exists
$r = q("SELECT `id` FROM `gcontact` WHERE `nurl` = '%s'", dbesc(normalise_link($orig_profile)));
if ($r) {
q("DELETE FROM `gcontact` WHERE `nurl` = '%s'", dbesc(normalise_link($orig_profile)));
q("DELETE FROM `glink` WHERE `gcid` = %d", intval($r[0]["id"]));
}
// possibly create a new entry
poco_check($profile_url, $name, $network, $profile_photo, $about, $location, $gender, $keywords, $connect_url, $updated, $generation, $cid, $uid, $zcid);
}
}
if ($alternate AND ($network == NETWORK_OSTATUS))
return $gcid;
if (count($x) AND ($x[0]["network"] == "") AND ($network != "")) {
q("UPDATE `gcontact` SET `network` = '%s' WHERE `nurl` = '%s'",
dbesc($network),
dbesc(normalise_link($profile_url))
);
if (!isset($gcontact['name']) OR !isset($gcontact['photo'])) {
throw new Exception('No name and photo for URL '.$gcontact['url']);
}
if (($name == "") OR ($profile_photo == ""))
return $gcid;
if (!in_array($gcontact['network'], array(NETWORK_DFRN, NETWORK_OSTATUS, NETWORK_DIASPORA))) {
throw new Exception('No federated network ('.$gcontact['network'].') detected for URL '.$gcontact['url']);
}
if (!in_array($network, array(NETWORK_DFRN, NETWORK_OSTATUS, NETWORK_DIASPORA)))
return $gcid;
if (!isset($gcontact['server_url'])) {
// We check the server url to be sure that it is a real one
$server_url = poco_detect_server($gcontact['url']);
logger("profile-check generation: ".$generation." Network: ".$network." URL: ".$profile_url." name: ".$name." avatar: ".$profile_photo, LOGGER_DEBUG);
// We check the server url to be sure that it is a real one
$server_url2 = poco_detect_server($profile_url);
// We are no sure that it is a correct URL. So we use it in the future
if ($server_url2 != "") {
$server_url = $server_url2;
// We are now sure that it is a correct URL. So we use it in the future
if ($server_url != "") {
$gcontact['server_url'] = $server_url;
}
}
// The server URL doesn't seem to be valid, so we don't store it.
if (!poco_check_server($server_url, $network)) {
$server_url = "";
if (!poco_check_server($gcontact['server_url'], $gcontact['network'])) {
$gcontact['server_url'] = "";
}
$gcontact = array("url" => $profile_url,
"addr" => $addr,
"alias" => $alias,
"name" => $name,
"network" => $network,
"photo" => $profile_photo,
"about" => $about,
"location" => $location,
"gender" => $gender,
"keywords" => $keywords,
"server_url" => $server_url,
"connect" => $connect_url,
"notify" => $notify,
"updated" => $updated,
"generation" => $generation);
return $gcontact;
}
$gcid = update_gcontact($gcontact);
/**
* @brief Link the gcontact entry with user, contact and global contact
*
* @param integer $gcid Global contact ID
* @param integer $cid Contact ID
* @param integer $uid User ID
* @param integer $zcid Global Contact ID
* *
*/
function link_gcontact($gcid, $uid = 0, $cid = 0, $zcid = 0) {
if(!$gcid)
return $gcid;
if ($gcid <= 0) {
return;
}
$r = q("SELECT * FROM `glink` WHERE `cid` = %d AND `uid` = %d AND `gcid` = %d AND `zcid` = %d LIMIT 1",
intval($cid),
@ -363,8 +349,8 @@ function poco_check($profile_url, $name, $network, $profile_photo, $about, $loca
intval($gcid),
intval($zcid)
);
if (! dbm::is_result($r)) {
q("INSERT INTO `glink` (`cid`,`uid`,`gcid`,`zcid`, `updated`) VALUES (%d,%d,%d,%d, '%s') ",
if (!dbm::is_result($r)) {
q("INSERT INTO `glink` (`cid`, `uid`, `gcid`, `zcid`, `updated`) VALUES (%d, %d, %d, %d, '%s') ",
intval($cid),
intval($uid),
intval($gcid),
@ -380,8 +366,6 @@ function poco_check($profile_url, $name, $network, $profile_photo, $about, $loca
intval($zcid)
);
}
return $gcid;
}
function poco_reachable($profile, $server = "", $network = "", $force = false) {
@ -479,15 +463,26 @@ function poco_last_updated($profile, $force = false) {
$gcontacts = q("SELECT * FROM `gcontact` WHERE `nurl` = '%s'",
dbesc(normalise_link($profile)));
if ($gcontacts[0]["created"] <= NULL_DATE) {
q("UPDATE `gcontact` SET `created` = '%s' WHERE `nurl` = '%s'",
dbesc(datetime_convert()), dbesc(normalise_link($profile)));
if (!dbm::is_result($gcontacts)) {
return false;
}
if ($gcontacts[0]["server_url"] != "") {
$contact = array("url" => $profile);
if ($gcontacts[0]["created"] <= NULL_DATE) {
$contact['created'] = datetime_convert();
}
if ($force) {
$server_url = normalise_link(poco_detect_server($profile));
}
if (($server_url == '') AND ($gcontacts[0]["server_url"] != "")) {
$server_url = $gcontacts[0]["server_url"];
}
if (($server_url == '') OR ($gcontacts[0]["server_url"] == $gcontacts[0]["nurl"])) {
$server_url = poco_detect_server($profile);
if (!$force AND (($server_url == '') OR ($gcontacts[0]["server_url"] == $gcontacts[0]["nurl"]))) {
$server_url = normalise_link(poco_detect_server($profile));
}
if (!in_array($gcontacts[0]["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_FEED, NETWORK_OSTATUS, ""))) {
@ -497,67 +492,64 @@ function poco_last_updated($profile, $force = false) {
if ($server_url != "") {
if (!poco_check_server($server_url, $gcontacts[0]["network"], $force)) {
if ($force)
if ($force) {
q("UPDATE `gcontact` SET `last_failure` = '%s' WHERE `nurl` = '%s'",
dbesc(datetime_convert()), dbesc(normalise_link($profile)));
}
logger("Profile ".$profile.": Server ".$server_url." wasn't reachable.", LOGGER_DEBUG);
return false;
}
q("UPDATE `gcontact` SET `server_url` = '%s' WHERE `nurl` = '%s'",
dbesc($server_url), dbesc(normalise_link($profile)));
$contact['server_url'] = $server_url;
}
if (in_array($gcontacts[0]["network"], array("", NETWORK_FEED))) {
$server = q("SELECT `network` FROM `gserver` WHERE `nurl` = '%s' AND `network` != ''",
dbesc(normalise_link($server_url)));
if ($server)
q("UPDATE `gcontact` SET `network` = '%s' WHERE `nurl` = '%s'",
dbesc($server[0]["network"]), dbesc(normalise_link($profile)));
else
if ($server) {
$contact['network'] = $server[0]["network"];
} else {
return false;
}
}
// noscrape is really fast so we don't cache the call.
if (($gcontacts[0]["server_url"] != "") AND ($gcontacts[0]["nick"] != "")) {
if (($server_url != "") AND ($gcontacts[0]["nick"] != "")) {
// Use noscrape if possible
$server = q("SELECT `noscrape`, `network` FROM `gserver` WHERE `nurl` = '%s' AND `noscrape` != ''", dbesc(normalise_link($gcontacts[0]["server_url"])));
$server = q("SELECT `noscrape`, `network` FROM `gserver` WHERE `nurl` = '%s' AND `noscrape` != ''", dbesc(normalise_link($server_url)));
if ($server) {
$noscraperet = z_fetch_url($server[0]["noscrape"]."/".$gcontacts[0]["nick"]);
if ($noscraperet["success"] AND ($noscraperet["body"] != "")) {
if ($noscraperet["success"] AND ($noscraperet["body"] != "")) {
$noscrape = json_decode($noscraperet["body"], true);
if (is_array($noscrape)) {
$contact = array("url" => $profile,
"network" => $server[0]["network"],
"generation" => $gcontacts[0]["generation"]);
$contact["network"] = $server[0]["network"];
if (isset($noscrape["fn"]))
if (isset($noscrape["fn"])) {
$contact["name"] = $noscrape["fn"];
if (isset($noscrape["comm"]))
}
if (isset($noscrape["comm"])) {
$contact["community"] = $noscrape["comm"];
}
if (isset($noscrape["tags"])) {
$keywords = implode(" ", $noscrape["tags"]);
if ($keywords != "")
if ($keywords != "") {
$contact["keywords"] = $keywords;
}
}
$location = formatted_location($noscrape);
if ($location)
if ($location) {
$contact["location"] = $location;
if (isset($noscrape["dfrn-notify"]))
}
if (isset($noscrape["dfrn-notify"])) {
$contact["notify"] = $noscrape["dfrn-notify"];
}
// Remove all fields that are not present in the gcontact table
unset($noscrape["fn"]);
unset($noscrape["key"]);
@ -595,12 +587,14 @@ function poco_last_updated($profile, $force = false) {
}
// If we only can poll the feed, then we only do this once a while
if (!$force AND !poco_do_update($gcontacts[0]["created"], $gcontacts[0]["updated"], $gcontacts[0]["last_failure"], $gcontacts[0]["last_contact"])) {
if (!$force AND !poco_do_update($gcontacts[0]["created"], $gcontacts[0]["updated"], $gcontacts[0]["last_failure"], $gcontacts[0]["last_contact"])) {
logger("Profile ".$profile." was last updated at ".$gcontacts[0]["updated"]." (cached)", LOGGER_DEBUG);
update_gcontact($contact);
return $gcontacts[0]["updated"];
}
$data = probe_url($profile);
$data = Probe::uri($profile);
// Is the profile link the alternate OStatus link notation? (http://domain.tld/user/4711)
// Then check the other link and delete this one
@ -612,10 +606,18 @@ function poco_last_updated($profile, $force = false) {
q("DELETE FROM `gcontact` WHERE `nurl` = '%s'", dbesc(normalise_link($profile)));
q("DELETE FROM `glink` WHERE `gcid` = %d", intval($gcontacts[0]["id"]));
poco_check($data["url"], $data["name"], $data["network"], $data["photo"], $gcontacts[0]["about"], $gcontacts[0]["location"],
$gcontacts[0]["gender"], $gcontacts[0]["keywords"], $data["addr"], $gcontacts[0]["updated"], $gcontacts[0]["generation"]);
$gcontact = array_merge($gcontacts[0], $data);
poco_last_updated($data["url"], $force);
$gcontact["server_url"] = $data["baseurl"];
try {
$gcontact = sanitize_gcontact($gcontact);
update_gcontact($gcontact);
poco_last_updated($data["url"], $force);
} catch (Exception $e) {
logger($e->getMessage(), LOGGER_DEBUG);
}
logger("Profile ".$profile." was deleted", LOGGER_DEBUG);
return false;
@ -629,21 +631,10 @@ function poco_last_updated($profile, $force = false) {
return false;
}
$contact = array("generation" => $gcontacts[0]["generation"]);
$contact = array_merge($contact, $data);
$contact["server_url"] = $data["baseurl"];
unset($contact["batch"]);
unset($contact["poll"]);
unset($contact["request"]);
unset($contact["confirm"]);
unset($contact["poco"]);
unset($contact["priority"]);
unset($contact["pubkey"]);
unset($contact["baseurl"]);
update_gcontact($contact);
$feedret = z_fetch_url($data["poll"]);
@ -686,9 +677,10 @@ function poco_last_updated($profile, $force = false) {
q("UPDATE `gcontact` SET `updated` = '%s', `last_contact` = '%s' WHERE `nurl` = '%s'",
dbesc(dbm::date($last_updated)), dbesc(dbm::date()), dbesc(normalise_link($profile)));
if (($gcontacts[0]["generation"] == 0))
if (($gcontacts[0]["generation"] == 0)) {
q("UPDATE `gcontact` SET `generation` = 9 WHERE `nurl` = '%s'",
dbesc(normalise_link($profile)));
}
logger("Profile ".$profile." was last updated at ".$last_updated, LOGGER_DEBUG);
@ -1769,7 +1761,7 @@ function poco_discover($complete = false) {
}
logger('Update directory from server '.$server['url'].' with ID '.$server['id'], LOGGER_DEBUG);
proc_run(PRIORITY_LOW, "include/discover_poco.php", "update_server_directory", $server['id']);
proc_run(PRIORITY_LOW, "include/discover_poco.php", "update_server_directory", intval($server['id']));
if (!$complete AND (--$no_of_queries == 0)) {
break;
@ -1889,10 +1881,26 @@ function poco_discover_server($data, $default_generation = 0) {
$success = true;
logger("Store profile ".$profile_url, LOGGER_DEBUG);
poco_check($profile_url, $name, $network, $profile_photo, $about, $location, $gender, $keywords, $connect_url, $updated, $generation, 0, 0, 0);
$gcontact = array("url" => $profile_url, "contact-type" => $contact_type, "generation" => $generation);
update_gcontact($gcontact);
$gcontact = array("url" => $profile_url,
"name" => $name,
"network" => $network,
"photo" => $profile_photo,
"about" => $about,
"location" => $location,
"gender" => $gender,
"keywords" => $keywords,
"connect" => $connect_url,
"updated" => $updated,
"contact-type" => $contact_type,
"generation" => $generation);
try {
$gcontact = sanitize_gcontact($gcontact);
update_gcontact($gcontact);
} catch (Exception $e) {
logger($e->getMessage(), LOGGER_DEBUG);
}
logger("Done for profile ".$profile_url, LOGGER_DEBUG);
}
@ -2187,6 +2195,8 @@ function update_gcontact_from_probe($url) {
return;
}
$data["server_url"] = $data["baseurl"];
update_gcontact($data);
}

View file

@ -97,13 +97,6 @@ function create_user($arr) {
if(mb_strlen($username) < 3)
$result['message'] .= t('Name too short.') . EOL;
// I don't really like having this rule, but it cuts down
// on the number of auto-registrations by Russian spammers
// Using preg_match was completely unreliable, due to mixed UTF-8 regex support
// $no_utf = get_config('system','no_utf');
// $pat = (($no_utf) ? '/^[a-zA-Z]* [a-zA-Z]*$/' : '/^\p{L}* \p{L}*$/u' );
// So now we are just looking for a space in the full name.
$loose_reg = get_config('system','no_regfullname');
@ -182,17 +175,7 @@ function create_user($arr) {
$prvkey = $keys['prvkey'];
$pubkey = $keys['pubkey'];
/**
*
* Create another keypair for signing/verifying
* salmon protocol messages. We have to use a slightly
* less robust key because this won't be using openssl
* but the phpseclib. Since it is PHP interpreted code
* it is not nearly as efficient, and the larger keys
* will take several minutes each to process.
*
*/
// Create another keypair for signing/verifying salmon protocol messages.
$sres = new_keypair(512);
$sprvkey = $sres['prvkey'];
$spubkey = $sres['pubkey'];

View file

@ -342,9 +342,9 @@ final class Crypto
*/
private static function SecureRandom($octets)
{
self::EnsureFunctionExists("mcrypt_create_iv");
$random = mcrypt_create_iv($octets, MCRYPT_DEV_URANDOM);
if ($random === FALSE) {
self::EnsureFunctionExists("openssl_random_pseudo_bytes");
$random = openssl_random_pseudo_bytes($octets, $crypto_strong);
if ($crypto_strong === FALSE) {
throw new CannotPerformOperationException();
} else {
return $random;

View file

@ -1,24 +1,17 @@
<?php
//# Install PSR-0-compatible class autoloader
//spl_autoload_register(function($class){
// require preg_replace('{\\\\|_(?!.*\\\\)}', DIRECTORY_SEPARATOR, ltrim($class, '\\')).'.php';
//});
require_once("library/php-markdown/Michelf/MarkdownExtra.inc.php");
# Get Markdown class
require_once "library/php-markdown/Michelf/MarkdownExtra.inc.php";
use \Michelf\MarkdownExtra;
function Markdown($text) {
$a = get_app();
$stamp1 = microtime(true);
# Read file and pass content through the Markdown parser
$html = MarkdownExtra::defaultTransform($text);
$MarkdownParser = new MarkdownExtra();
$MarkdownParser->hard_wrap = true;
$html = $MarkdownParser->transform($text);
$a->save_timestamp($stamp1, "parser");
return $html;
}
?>

View file

@ -1,728 +0,0 @@
<?php
//ini_set('display_errors', 1);
//error_reporting(E_ALL | E_STRICT);
// Regex to filter out the client identifier
// (described in Section 2 of IETF draft)
// IETF draft does not prescribe a format for these, however
// I've arbitrarily chosen alphanumeric strings with hyphens and underscores, 3-12 characters long
// Feel free to change.
define("REGEX_CLIENT_ID", "/^[a-z0-9-_]{3,12}$/i");
// Used to define the name of the OAuth access token parameter (POST/GET/etc.)
// IETF Draft sections 5.2 and 5.3 specify that it should be called "oauth_token"
// but other implementations use things like "access_token"
// I won't be heartbroken if you change it, but it might be better to adhere to the spec
define("OAUTH_TOKEN_PARAM_NAME", "oauth_token");
// Client types (for client authorization)
//define("WEB_SERVER_CLIENT_TYPE", "web_server");
//define("USER_AGENT_CLIENT_TYPE", "user_agent");
//define("REGEX_CLIENT_TYPE", "/^(web_server|user_agent)$/");
define("ACCESS_TOKEN_AUTH_RESPONSE_TYPE", "token");
define("AUTH_CODE_AUTH_RESPONSE_TYPE", "code");
define("CODE_AND_TOKEN_AUTH_RESPONSE_TYPE", "code-and-token");
define("REGEX_AUTH_RESPONSE_TYPE", "/^(token|code|code-and-token)$/");
// Grant Types (for token obtaining)
define("AUTH_CODE_GRANT_TYPE", "authorization-code");
define("USER_CREDENTIALS_GRANT_TYPE", "basic-credentials");
define("ASSERTION_GRANT_TYPE", "assertion");
define("REFRESH_TOKEN_GRANT_TYPE", "refresh-token");
define("NONE_GRANT_TYPE", "none");
define("REGEX_TOKEN_GRANT_TYPE", "/^(authorization-code|basic-credentials|assertion|refresh-token|none)$/");
/* Error handling constants */
// HTTP status codes
define("ERROR_NOT_FOUND", "404 Not Found");
define("ERROR_BAD_REQUEST", "400 Bad Request");
// TODO: Extend for i18n
// "Official" OAuth 2.0 errors
define("ERROR_REDIRECT_URI_MISMATCH", "redirect-uri-mismatch");
define("ERROR_INVALID_CLIENT_CREDENTIALS", "invalid-client-credentials");
define("ERROR_UNAUTHORIZED_CLIENT", "unauthorized-client");
define("ERROR_USER_DENIED", "access-denied");
define("ERROR_INVALID_REQUEST", "invalid-request");
define("ERROR_INVALID_CLIENT_ID", "invalid-client-id");
define("ERROR_UNSUPPORTED_RESPONSE_TYPE", "unsupported-response-type");
define("ERROR_INVALID_SCOPE", "invalid-scope");
define("ERROR_INVALID_GRANT", "invalid-grant");
// Protected resource errors
define("ERROR_INVALID_TOKEN", "invalid-token");
define("ERROR_EXPIRED_TOKEN", "expired-token");
define("ERROR_INSUFFICIENT_SCOPE", "insufficient-scope");
// Messages
define("ERROR_INVALID_RESPONSE_TYPE", "Invalid response type.");
// Errors that we made up
// Error for trying to use a grant type that we haven't implemented
define("ERROR_UNSUPPORTED_GRANT_TYPE", "unsupported-grant-type");
abstract class OAuth2 {
/* Subclasses must implement the following functions */
// Make sure that the client id is valid
// If a secret is required, check that they've given the right one
// Must return false if the client credentials are invalid
abstract protected function auth_client_credentials($client_id, $client_secret = null);
// OAuth says we should store request URIs for each registered client
// Implement this function to grab the stored URI for a given client id
// Must return false if the given client does not exist or is invalid
abstract protected function get_redirect_uri($client_id);
// We need to store and retrieve access token data as we create and verify tokens
// Implement these functions to do just that
// Look up the supplied token id from storage, and return an array like:
//
// array(
// "client_id" => <stored client id>,
// "expires" => <stored expiration timestamp>,
// "scope" => <stored scope (may be null)
// )
//
// Return null if the supplied token is invalid
//
abstract protected function get_access_token($token_id);
// Store the supplied values
abstract protected function store_access_token($token_id, $client_id, $expires, $scope = null);
/*
*
* Stuff that should get overridden by subclasses
*
* I don't want to make these abstract, because then subclasses would have
* to implement all of them, which is too much work.
*
* So they're just stubs. Override the ones you need.
*
*/
// You should override this function with something,
// or else your OAuth provider won't support any grant types!
protected function get_supported_grant_types() {
// If you support all grant types, then you'd do:
// return array(
// AUTH_CODE_GRANT_TYPE,
// USER_CREDENTIALS_GRANT_TYPE,
// ASSERTION_GRANT_TYPE,
// REFRESH_TOKEN_GRANT_TYPE,
// NONE_GRANT_TYPE
// );
return array();
}
// You should override this function with your supported response types
protected function get_supported_auth_response_types() {
return array(
AUTH_CODE_AUTH_RESPONSE_TYPE,
ACCESS_TOKEN_AUTH_RESPONSE_TYPE,
CODE_AND_TOKEN_AUTH_RESPONSE_TYPE
);
}
// If you want to support scope use, then have this function return a list
// of all acceptable scopes (used to throw the invalid-scope error)
protected function get_supported_scopes() {
// Example:
// return array("my-friends", "photos", "whatever-else");
return array();
}
// If you want to restrict clients to certain authorization response types,
// override this function
// Given a client identifier and auth type, return true or false
// (auth type would be one of the values contained in REGEX_AUTH_RESPONSE_TYPE)
protected function authorize_client_response_type($client_id, $response_type) {
return true;
}
// If you want to restrict clients to certain grant types, override this function
// Given a client identifier and grant type, return true or false
protected function authorize_client($client_id, $grant_type) {
return true;
}
/* Functions that help grant access tokens for various grant types */
// Fetch authorization code data (probably the most common grant type)
// IETF Draft 4.1.1: http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-4.1.1
// Required for AUTH_CODE_GRANT_TYPE
protected function get_stored_auth_code($code) {
// Retrieve the stored data for the given authorization code
// Should return:
//
// array (
// "client_id" => <stored client id>,
// "redirect_uri" => <stored redirect URI>,
// "expires" => <stored code expiration time>,
// "scope" => <stored scope values (space-separated string), or can be omitted if scope is unused>
// )
//
// Return null if the code is invalid.
return null;
}
// Take the provided authorization code values and store them somewhere (db, etc.)
// Required for AUTH_CODE_GRANT_TYPE
protected function store_auth_code($code, $client_id, $redirect_uri, $expires, $scope) {
// This function should be the storage counterpart to get_stored_auth_code
// If storage fails for some reason, we're not currently checking
// for any sort of success/failure, so you should bail out of the
// script and provide a descriptive fail message
}
// Grant access tokens for basic user credentials
// IETF Draft 4.1.2: http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-4.1.2
// Required for USER_CREDENTIALS_GRANT_TYPE
protected function check_user_credentials($client_id, $username, $password) {
// Check the supplied username and password for validity
// You can also use the $client_id param to do any checks required
// based on a client, if you need that
// If the username and password are invalid, return false
// If the username and password are valid, and you want to verify the scope of
// a user's access, return an array with the scope values, like so:
//
// array (
// "scope" => <stored scope values (space-separated string)>
// )
//
// We'll check the scope you provide against the requested scope before
// providing an access token.
//
// Otherwise, just return true.
return false;
}
// Grant access tokens for assertions
// IETF Draft 4.1.3: http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-4.1.3
// Required for ASSERTION_GRANT_TYPE
protected function check_assertion($client_id, $assertion_type, $assertion) {
// Check the supplied assertion for validity
// You can also use the $client_id param to do any checks required
// based on a client, if you need that
// If the assertion is invalid, return false
// If the assertion is valid, and you want to verify the scope of
// an access request, return an array with the scope values, like so:
//
// array (
// "scope" => <stored scope values (space-separated string)>
// )
//
// We'll check the scope you provide against the requested scope before
// providing an access token.
//
// Otherwise, just return true.
return false;
}
// Grant refresh access tokens
// IETF Draft 4.1.4: http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-4.1.4
// Required for REFRESH_TOKEN_GRANT_TYPE
protected function get_refresh_token($refresh_token) {
// Retrieve the stored data for the given refresh token
// Should return:
//
// array (
// "client_id" => <stored client id>,
// "expires" => <refresh token expiration time>,
// "scope" => <stored scope values (space-separated string), or can be omitted if scope is unused>
// )
//
// Return null if the token id is invalid.
return null;
}
// Store refresh access tokens
// Required for REFRESH_TOKEN_GRANT_TYPE
protected function store_refresh_token($token, $client_id, $expires, $scope = null) {
// If storage fails for some reason, we're not currently checking
// for any sort of success/failure, so you should bail out of the
// script and provide a descriptive fail message
return;
}
// Grant access tokens for the "none" grant type
// Not really described in the IETF Draft, so I just left a method stub...do whatever you want!
// Required for NONE_GRANT_TYPE
protected function check_none_access($client_id) {
return false;
}
protected function get_default_authentication_realm() {
// Change this to whatever authentication realm you want to send in a WWW-Authenticate header
return "Service";
}
/* End stuff that should get overridden */
private $access_token_lifetime = 3600;
private $auth_code_lifetime = 30;
private $refresh_token_lifetime = 1209600; // Two weeks
public function __construct($access_token_lifetime = 3600, $auth_code_lifetime = 30, $refresh_token_lifetime = 1209600) {
$this->access_token_lifetime = $access_token_lifetime;
$this->auth_code_lifetime = $auth_code_lifetime;
$this->refresh_token_lifetime = $refresh_token_lifetime;
}
/* Resource protecting (Section 5) */
// Check that a valid access token has been provided
//
// The scope parameter defines any required scope that the token must have
// If a scope param is provided and the token does not have the required scope,
// we bounce the request
//
// Some implementations may choose to return a subset of the protected resource
// (i.e. "public" data) if the user has not provided an access token
// or if the access token is invalid or expired
//
// The IETF spec says that we should send a 401 Unauthorized header and bail immediately
// so that's what the defaults are set to
//
// Here's what each parameter does:
// $scope = A space-separated string of required scope(s), if you want to check for scope
// $exit_not_present = If true and no access token is provided, send a 401 header and exit, otherwise return false
// $exit_invalid = If true and the implementation of get_access_token returns null, exit, otherwise return false
// $exit_expired = If true and the access token has expired, exit, otherwise return false
// $exit_scope = If true the access token does not have the required scope(s), exit, otherwise return false
// $realm = If you want to specify a particular realm for the WWW-Authenticate header, supply it here
public function verify_access_token($scope = null, $exit_not_present = true, $exit_invalid = true, $exit_expired = true, $exit_scope = true, $realm = null) {
$token_param = $this->get_access_token_param();
if ($token_param === false) // Access token was not provided
return $exit_not_present ? $this->send_401_unauthorized($realm, $scope) : false;
// Get the stored token data (from the implementing subclass)
$token = $this->get_access_token($token_param);
if ($token === null)
return $exit_invalid ? $this->send_401_unauthorized($realm, $scope, ERROR_INVALID_TOKEN) : false;
// Check token expiration (I'm leaving this check separated, later we'll fill in better error messages)
if (isset($token["expires"]) && time() > $token["expires"])
return $exit_expired ? $this->send_401_unauthorized($realm, $scope, ERROR_EXPIRED_TOKEN) : false;
// Check scope, if provided
// If token doesn't have a scope, it's null/empty, or it's insufficient, then throw an error
if ($scope &&
(!isset($token["scope"]) || !$token["scope"] || !$this->check_scope($scope, $token["scope"])))
return $exit_scope ? $this->send_401_unauthorized($realm, $scope, ERROR_INSUFFICIENT_SCOPE) : false;
return true;
}
// Returns true if everything in required scope is contained in available scope
// False if something in required scope is not in available scope
private function check_scope($required_scope, $available_scope) {
// The required scope should match or be a subset of the available scope
if (!is_array($required_scope))
$required_scope = explode(" ", $required_scope);
if (!is_array($available_scope))
$available_scope = explode(" ", $available_scope);
return (count(array_diff($required_scope, $available_scope)) == 0);
}
// Send a 401 unauthorized header with the given realm
// and an error, if provided
private function send_401_unauthorized($realm, $scope, $error = null) {
$realm = $realm === null ? $this->get_default_authentication_realm() : $realm;
$auth_header = "WWW-Authenticate: Token realm='".$realm."'";
if ($scope)
$auth_header .= ", scope='".$scope."'";
if ($error !== null)
$auth_header .= ", error='".$error."'";
header("HTTP/1.1 401 Unauthorized");
header($auth_header);
exit;
}
// Pulls the access token out of the HTTP request
// Either from the Authorization header or GET/POST/etc.
// Returns false if no token is present
// TODO: Support POST or DELETE
private function get_access_token_param() {
$auth_header = $this->get_authorization_header();
if ($auth_header !== false) {
// Make sure only the auth header is set
if (isset($_GET[OAUTH_TOKEN_PARAM_NAME]) || isset($_POST[OAUTH_TOKEN_PARAM_NAME]))
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
$auth_header = trim($auth_header);
// Make sure it's Token authorization
if (strcmp(substr($auth_header, 0, 6),"Token ") !== 0)
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
// Parse the rest of the header
if (preg_match('/\s*token\s*="(.+)"/', substr($auth_header, 6), $matches) == 0 || count($matches) < 2)
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
return $matches[1];
}
if (isset($_GET[OAUTH_TOKEN_PARAM_NAME])) {
if (isset($_POST[OAUTH_TOKEN_PARAM_NAME])) // Both GET and POST are not allowed
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
return $_GET[OAUTH_TOKEN_PARAM_NAME];
}
if (isset($_POST[OAUTH_TOKEN_PARAM_NAME]))
return $_POST[OAUTH_TOKEN_PARAM_NAME];
return false;
}
/* Access token granting (Section 4) */
// Grant or deny a requested access token
// This would be called from the "/token" endpoint as defined in the spec
// Obviously, you can call your endpoint whatever you want
public function grant_access_token() {
$filters = array(
"grant_type" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => REGEX_TOKEN_GRANT_TYPE), "flags" => FILTER_REQUIRE_SCALAR),
"scope" => array("flags" => FILTER_REQUIRE_SCALAR),
"code" => array("flags" => FILTER_REQUIRE_SCALAR),
"redirect_uri" => array("filter" => FILTER_VALIDATE_URL, "flags" => array(FILTER_FLAG_SCHEME_REQUIRED, FILTER_REQUIRE_SCALAR)),
"username" => array("flags" => FILTER_REQUIRE_SCALAR),
"password" => array("flags" => FILTER_REQUIRE_SCALAR),
"assertion_type" => array("flags" => FILTER_REQUIRE_SCALAR),
"assertion" => array("flags" => FILTER_REQUIRE_SCALAR),
"refresh_token" => array("flags" => FILTER_REQUIRE_SCALAR),
);
$input = filter_input_array(INPUT_POST, $filters);
// Grant Type must be specified.
if (!$input["grant_type"])
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
// Make sure we've implemented the requested grant type
if (!in_array($input["grant_type"], $this->get_supported_grant_types()))
$this->error(ERROR_BAD_REQUEST, ERROR_UNSUPPORTED_GRANT_TYPE);
// Authorize the client
$client = $this->get_client_credentials();
if ($this->auth_client_credentials($client[0], $client[1]) === false)
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_CLIENT_CREDENTIALS);
if (!$this->authorize_client($client[0], $input["grant_type"]))
$this->error(ERROR_BAD_REQUEST, ERROR_UNAUTHORIZED_CLIENT);
// Do the granting
switch ($input["grant_type"]) {
case AUTH_CODE_GRANT_TYPE:
if (!$input["code"] || !$input["redirect_uri"])
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
$stored = $this->get_stored_auth_code($input["code"]);
if ($stored === null || $input["redirect_uri"] != $stored["redirect_uri"] || $client[0] != $stored["client_id"])
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT);
if ($stored["expires"] > time())
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT);
break;
case USER_CREDENTIALS_GRANT_TYPE:
if (!$input["username"] || !$input["password"])
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
$stored = $this->check_user_credentials($client[0], $input["username"], $input["password"]);
if ($stored === false)
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT);
break;
case ASSERTION_GRANT_TYPE:
if (!$input["assertion_type"] || !$input["assertion"])
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
$stored = $this->check_assertion($client[0], $input["assertion_type"], $input["assertion"]);
if ($stored === false)
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT);
break;
case REFRESH_TOKEN_GRANT_TYPE:
if (!$input["refresh_token"])
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
$stored = $this->get_refresh_token($input["refresh_token"]);
if ($stored === null || $client[0] != $stored["client_id"])
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT);
if ($stored["expires"] > time())
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_GRANT);
break;
case NONE_GRANT_TYPE:
$stored = $this->check_none_access($client[0]);
if ($stored === false)
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
}
// Check scope, if provided
if ($input["scope"] && (!is_array($stored) || !isset($stored["scope"]) || !$this->check_scope($input["scope"], $stored["scope"])))
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_SCOPE);
if (!$input["scope"])
$input["scope"] = null;
$token = $this->create_access_token($client[0], $input["scope"]);
$this->send_json_headers();
echo json_encode($token);
}
// Internal function used to get the client credentials from HTTP basic auth or POST data
// See http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-2
private function get_client_credentials() {
if (isset($_SERVER["PHP_AUTH_USER"]) && $_POST && isset($_POST["client_id"]))
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_CLIENT_CREDENTIALS);
// Try basic auth
if (isset($_SERVER["PHP_AUTH_USER"]))
return array($_SERVER["PHP_AUTH_USER"], $_SERVER["PHP_AUTH_PW"]);
// Try POST
if ($_POST && isset($_POST["client_id"])) {
if (isset($_POST["client_secret"]))
return array($_POST["client_id"], $_POST["client_secret"]);
return array($_POST["client_id"], NULL);
}
// No credentials were specified
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_CLIENT_CREDENTIALS);
}
/* End-user/client Authorization (Section 3 of IETF Draft) */
// Pull the authorization request data out of the HTTP request
// and return it so the authorization server can prompt the user
// for approval
public function get_authorize_params() {
$filters = array(
"client_id" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => REGEX_CLIENT_ID), "flags" => FILTER_REQUIRE_SCALAR),
"response_type" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => REGEX_AUTH_RESPONSE_TYPE), "flags" => FILTER_REQUIRE_SCALAR),
"redirect_uri" => array("filter" => FILTER_VALIDATE_URL, "flags" => array(FILTER_FLAG_SCHEME_REQUIRED, FILTER_REQUIRE_SCALAR)),
"state" => array("flags" => FILTER_REQUIRE_SCALAR),
"scope" => array("flags" => FILTER_REQUIRE_SCALAR),
);
$input = filter_input_array(INPUT_GET, $filters);
// Make sure a valid client id was supplied
if (!$input["client_id"]) {
if ($input["redirect_uri"])
$this->callback_error($input["redirect_uri"], ERROR_INVALID_CLIENT_ID, $input["state"]);
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_CLIENT_ID); // We don't have a good URI to use
}
// redirect_uri is not required if already established via other channels
// check an existing redirect URI against the one supplied
$redirect_uri = $this->get_redirect_uri($input["client_id"]);
// At least one of: existing redirect URI or input redirect URI must be specified
if (!$redirect_uri && !$input["redirect_uri"])
$this->error(ERROR_BAD_REQUEST, ERROR_INVALID_REQUEST);
// get_redirect_uri should return false if the given client ID is invalid
// this probably saves us from making a separate db call, and simplifies the method set
if ($redirect_uri === false)
$this->callback_error($input["redirect_uri"], ERROR_INVALID_CLIENT_ID, $input["state"]);
// If there's an existing uri and one from input, verify that they match
if ($redirect_uri && $input["redirect_uri"]) {
// Ensure that the input uri starts with the stored uri
if (strcasecmp(substr($input["redirect_uri"], 0, strlen($redirect_uri)),$redirect_uri) !== 0)
$this->callback_error($input["redirect_uri"], ERROR_REDIRECT_URI_MISMATCH, $input["state"]);
} elseif ($redirect_uri) { // They did not provide a uri from input, so use the stored one
$input["redirect_uri"] = $redirect_uri;
}
// type and client_id are required
if (!$input["response_type"])
$this->callback_error($input["redirect_uri"], ERROR_INVALID_REQUEST, $input["state"], ERROR_INVALID_RESPONSE_TYPE);
// Check requested auth response type against the list of supported types
if (array_search($input["response_type"], $this->get_supported_auth_response_types()) === false)
$this->callback_error($input["redirect_uri"], ERROR_UNSUPPORTED_RESPONSE_TYPE, $input["state"]);
// Validate that the requested scope is supported
if ($input["scope"] && !$this->check_scope($input["scope"], $this->get_supported_scopes()))
$this->callback_error($input["redirect_uri"], ERROR_INVALID_SCOPE, $input["state"]);
return $input;
}
// After the user has approved or denied the access request
// the authorization server should call this function to redirect
// the user appropriately
// The params all come from the results of get_authorize_params
// except for $is_authorized -- this is true or false depending on whether
// the user authorized the access
public function finish_client_authorization($is_authorized, $type, $client_id, $redirect_uri, $state, $scope = null) {
if ($state !== null)
$result["query"]["state"] = $state;
if ($is_authorized === false) {
$result["query"]["error"] = ERROR_USER_DENIED;
} else {
if ($type == AUTH_CODE_AUTH_RESPONSE_TYPE || $type == CODE_AND_TOKEN_AUTH_RESPONSE_TYPE)
$result["query"]["code"] = $this->create_auth_code($client_id, $redirect_uri, $scope);
if ($type == ACCESS_TOKEN_AUTH_RESPONSE_TYPE || $type == CODE_AND_TOKEN_AUTH_RESPONSE_TYPE)
$result["fragment"] = $this->create_access_token($client_id, $scope);
}
$this->do_redirect_uri_callback($redirect_uri, $result);
}
/* Other/utility functions */
private function do_redirect_uri_callback($redirect_uri, $result) {
header("HTTP/1.1 302 Found");
header("Location: " . $this->build_uri($redirect_uri, $result));
exit;
}
private function build_uri($uri, $data) {
$parse_url = parse_url($uri);
// Add our data to the parsed uri
foreach ($data as $k => $v) {
if (isset($parse_url[$k]))
$parse_url[$k] .= "&" . http_build_query($v);
else
$parse_url[$k] = http_build_query($v);
}
// Put humpty dumpty back together
return
((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "")
.((isset($parse_url["user"])) ? $parse_url["user"] . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") ."@" : "")
.((isset($parse_url["host"])) ? $parse_url["host"] : "")
.((isset($parse_url["port"])) ? ":" . $parse_url["port"] : "")
.((isset($parse_url["path"])) ? $parse_url["path"] : "")
.((isset($parse_url["query"])) ? "?" . $parse_url["query"] : "")
.((isset($parse_url["fragment"])) ? "#" . $parse_url["fragment"] : "");
}
// This belongs in a separate factory, but to keep it simple, I'm just keeping it here.
private function create_access_token($client_id, $scope) {
$token = array(
"access_token" => $this->gen_access_token(),
"expires_in" => $this->access_token_lifetime,
"scope" => $scope
);
$this->store_access_token($token["access_token"], $client_id, time() + $this->access_token_lifetime, $scope);
// Issue a refresh token also, if we support them
if (in_array(REFRESH_TOKEN_GRANT_TYPE, $this->get_supported_grant_types())) {
$token["refresh_token"] = $this->gen_access_token();
$this->store_refresh_token($token["refresh_token"], $client_id, time() + $this->refresh_token_lifetime, $scope);
}
return $token;
}
private function create_auth_code($client_id, $redirect_uri, $scope) {
$code = $this->gen_auth_code();
$this->store_auth_code($code, $client_id, $redirect_uri, time() + $this->auth_code_lifetime, $scope);
return $code;
}
// Implementing classes may want to override these two functions
// to implement other access token or auth code generation schemes
private function gen_access_token() {
return base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand()));
}
private function gen_auth_code() {
return base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand()));
}
// Implementing classes may need to override this function for use on non-Apache web servers
// Just pull out the Authorization HTTP header and return it
// Return false if the Authorization header does not exist
private function get_authorization_header() {
if (array_key_exists("HTTP_AUTHORIZATION", $_SERVER))
return $_SERVER["HTTP_AUTHORIZATION"];
if (function_exists("apache_request_headers")) {
$headers = apache_request_headers();
if (array_key_exists("Authorization", $headers))
return $headers["Authorization"];
}
return false;
}
private function send_json_headers() {
header("Content-Type: application/json");
header("Cache-Control: no-store");
}
public function error($code, $message = null) {
header("HTTP/1.1 " . $code);
if ($message) {
$this->send_json_headers();
echo json_encode(array("error" => $message));
}
exit;
}
public function callback_error($redirect_uri, $error, $state, $message = null, $error_uri = null) {
$result["query"]["error"] = $error;
if ($state)
$result["query"]["state"] = $state;
if ($message)
$result["query"]["error_description"] = $message;
if ($error_uri)
$result["query"]["error_uri"] = $error_uri;
$this->do_redirect_uri_callback($redirect_uri, $result);
}
}

View file

@ -1,11 +1,11 @@
PHP Markdown Lib
Copyright (c) 2004-2014 Michel Fortin
<http://michelf.ca/>
Copyright (c) 2004-2016 Michel Fortin
<https://michelf.ca/>
All rights reserved.
Based on Markdown
Copyright (c) 2003-2006 John Gruber
<http://daringfireball.net/>
<https://daringfireball.net/>
All rights reserved.
Redistribution and use in source and binary forms, with or without

View file

@ -1,10 +1,10 @@
<?php
# Use this file if you cannot use class autoloading. It will include all the
# files needed for the Markdown parser.
#
# Take a look at the PSR-0-compatible class autoloading implementation
# in the Readme.php file if you want a simple autoloader setup.
// Use this file if you cannot use class autoloading. It will include all the
// files needed for the Markdown parser.
//
// Take a look at the PSR-0-compatible class autoloading implementation
// in the Readme.php file if you want a simple autoloader setup.
require_once dirname(__FILE__) . '/MarkdownInterface.php';
require_once dirname(__FILE__) . '/Markdown.php';

View file

@ -1,147 +1,227 @@
<?php
#
# Markdown - A text-to-HTML conversion tool for web writers
#
# PHP Markdown
# Copyright (c) 2004-2014 Michel Fortin
# <http://michelf.com/projects/php-markdown/>
#
# Original Markdown
# Copyright (c) 2004-2006 John Gruber
# <http://daringfireball.net/projects/markdown/>
#
/**
* Markdown - A text-to-HTML conversion tool for web writers
*
* @package php-markdown
* @author Michel Fortin <michel.fortin@michelf.com>
* @copyright 2004-2016 Michel Fortin <https://michelf.com/projects/php-markdown/>
* @copyright (Original Markdown) 2004-2006 John Gruber <https://daringfireball.net/projects/markdown/>
*/
namespace Michelf;
#
# Markdown Parser Class
#
/**
* Markdown Parser Class
*/
class Markdown implements MarkdownInterface {
/**
* Define the package version
* @var string
*/
const MARKDOWNLIB_VERSION = "1.7.0";
### Version ###
const MARKDOWNLIB_VERSION = "1.4.1";
### Simple Function Interface ###
/**
* Simple function interface - Initialize the parser and return the result
* of its transform method. This will work fine for derived classes too.
*
* @api
*
* @param string $text
* @return string
*/
public static function defaultTransform($text) {
#
# Initialize the parser and return the result of its transform method.
# This will work fine for derived classes too.
#
# Take parser class on which this function was called.
// Take parser class on which this function was called.
$parser_class = \get_called_class();
# try to take parser from the static parser list
// Try to take parser from the static parser list
static $parser_list;
$parser =& $parser_list[$parser_class];
# create the parser it not already set
if (!$parser)
// Create the parser it not already set
if (!$parser) {
$parser = new $parser_class;
}
# Transform text using parser.
// Transform text using parser.
return $parser->transform($text);
}
### Configuration Variables ###
/**
* Configuration variables
*/
# Change to ">" for HTML output.
/**
* Change to ">" for HTML output.
* @var string
*/
public $empty_element_suffix = " />";
/**
* The width of indentation of the output markup
* @var int
*/
public $tab_width = 4;
# Change to `true` to disallow markup or entities.
public $no_markup = false;
/**
* Change to `true` to disallow markup or entities.
* @var boolean
*/
public $no_markup = false;
public $no_entities = false;
# Predefined urls and titles for reference links and images.
public $predef_urls = array();
/**
* Change to `true` to enable line breaks on \n without two trailling spaces
* @var boolean
*/
public $hard_wrap = false;
/**
* Predefined URLs and titles for reference links and images.
* @var array
*/
public $predef_urls = array();
public $predef_titles = array();
# Optional filter function for URLs
/**
* Optional filter function for URLs
* @var callable
*/
public $url_filter_func = null;
/**
* Optional header id="" generation callback function.
* @var callable
*/
public $header_id_func = null;
### Parser Implementation ###
/**
* Optional function for converting code block content to HTML
* @var callable
*/
public $code_block_content_func = null;
# Regex to match balanced [brackets].
# Needed to insert a maximum bracked depth while converting to PHP.
/**
* Optional function for converting code span content to HTML.
* @var callable
*/
public $code_span_content_func = null;
/**
* Class attribute to toggle "enhanced ordered list" behaviour
* setting this to true will allow ordered lists to start from the index
* number that is defined first.
*
* For example:
* 2. List item two
* 3. List item three
*
* Becomes:
* <ol start="2">
* <li>List item two</li>
* <li>List item three</li>
* </ol>
*
* @var bool
*/
public $enhanced_ordered_list = false;
/**
* Parser implementation
*/
/**
* Regex to match balanced [brackets].
* Needed to insert a maximum bracked depth while converting to PHP.
* @var int
*/
protected $nested_brackets_depth = 6;
protected $nested_brackets_re;
protected $nested_url_parenthesis_depth = 4;
protected $nested_url_parenthesis_re;
# Table of hash values for escaped characters:
/**
* Table of hash values for escaped characters:
* @var string
*/
protected $escape_chars = '\`*_{}[]()>#+-.!';
protected $escape_chars_re;
/**
* Constructor function. Initialize appropriate member variables.
* @return void
*/
public function __construct() {
#
# Constructor function. Initialize appropriate member variables.
#
$this->_initDetab();
$this->prepareItalicsAndBold();
$this->nested_brackets_re =
$this->nested_brackets_re =
str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth).
str_repeat('\])*', $this->nested_brackets_depth);
$this->nested_url_parenthesis_re =
$this->nested_url_parenthesis_re =
str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth).
str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth);
$this->escape_chars_re = '['.preg_quote($this->escape_chars).']';
# Sort document, block, and span gamut in ascendent priority order.
// Sort document, block, and span gamut in ascendent priority order.
asort($this->document_gamut);
asort($this->block_gamut);
asort($this->span_gamut);
}
# Internal hashes used during transformation.
protected $urls = array();
protected $titles = array();
/**
* Internal hashes used during transformation.
* @var array
*/
protected $urls = array();
protected $titles = array();
protected $html_hashes = array();
# Status flag to avoid invalid nesting.
/**
* Status flag to avoid invalid nesting.
* @var boolean
*/
protected $in_anchor = false;
/**
* Called before the transformation process starts to setup parser states.
* @return void
*/
protected function setup() {
#
# Called before the transformation process starts to setup parser
# states.
#
# Clear global hashes.
$this->urls = $this->predef_urls;
$this->titles = $this->predef_titles;
// Clear global hashes.
$this->urls = $this->predef_urls;
$this->titles = $this->predef_titles;
$this->html_hashes = array();
$this->in_anchor = false;
$this->in_anchor = false;
}
/**
* Called after the transformation process to clear any variable which may
* be taking up memory unnecessarly.
* @return void
*/
protected function teardown() {
#
# Called after the transformation process to clear any variable
# which may be taking up memory unnecessarly.
#
$this->urls = array();
$this->titles = array();
$this->urls = array();
$this->titles = array();
$this->html_hashes = array();
}
/**
* Main function. Performs some preprocessing on the input text and pass
* it through the document gamut.
*
* @api
*
* @param string $text
* @return string
*/
public function transform($text) {
#
# Main function. Performs some preprocessing on the input text
# and pass it through the document gamut.
#
$this->setup();
# Remove UTF-8 BOM and marker character in input, if present.
$text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text);
@ -168,28 +248,33 @@ class Markdown implements MarkdownInterface {
foreach ($this->document_gamut as $method => $priority) {
$text = $this->$method($text);
}
$this->teardown();
return $text . "\n";
}
/**
* Define the document gamut
* @var array
*/
protected $document_gamut = array(
# Strip link definitions, store in hashes.
// Strip link definitions, store in hashes.
"stripLinkDefinitions" => 20,
"runBasicBlockGamut" => 30,
);
);
/**
* Strips link definitions from text, stores the URLs and titles in
* hash references
* @param string $text
* @return string
*/
protected function stripLinkDefinitions($text) {
#
# Strips link definitions from text, stores the URLs and titles in
# hash references.
#
$less_than_tab = $this->tab_width - 1;
# Link defs are in the form: ^[id]: url "optional title"
// Link defs are in the form: ^[id]: url "optional title"
$text = preg_replace_callback('{
^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1
[ ]*
@ -213,43 +298,58 @@ class Markdown implements MarkdownInterface {
(?:\n+|\Z)
}xm',
array($this, '_stripLinkDefinitions_callback'),
$text);
$text
);
return $text;
}
/**
* The callback to strip link definitions
* @param array $matches
* @return string
*/
protected function _stripLinkDefinitions_callback($matches) {
$link_id = strtolower($matches[1]);
$url = $matches[2] == '' ? $matches[3] : $matches[2];
$this->urls[$link_id] = $url;
$this->titles[$link_id] =& $matches[4];
return ''; # String that will replace the block
return ''; // String that will replace the block
}
/**
* Hashify HTML blocks
* @param string $text
* @return string
*/
protected function hashHTMLBlocks($text) {
if ($this->no_markup) return $text;
if ($this->no_markup) {
return $text;
}
$less_than_tab = $this->tab_width - 1;
# Hashify HTML blocks:
# We only want to do this for block-level HTML tags, such as headers,
# lists, and tables. That's because we still want to wrap <p>s around
# "paragraphs" that are wrapped in non-block-level tags, such as anchors,
# phrase emphasis, and spans. The list of tags we're looking for is
# hard-coded:
#
# * List "a" is made of tags which can be both inline or block-level.
# These will be treated block-level when the start tag is alone on
# its line, otherwise they're not matched here and will be taken as
# inline later.
# * List "b" is made of tags which are always block-level;
#
/**
* Hashify HTML blocks:
*
* We only want to do this for block-level HTML tags, such as headers,
* lists, and tables. That's because we still want to wrap <p>s around
* "paragraphs" that are wrapped in non-block-level tags, such as
* anchors, phrase emphasis, and spans. The list of tags we're looking
* for is hard-coded:
*
* * List "a" is made of tags which can be both inline or block-level.
* These will be treated block-level when the start tag is alone on
* its line, otherwise they're not matched here and will be taken as
* inline later.
* * List "b" is made of tags which are always block-level;
*/
$block_tags_a_re = 'ins|del';
$block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'.
'script|noscript|style|form|fieldset|iframe|math|svg|'.
'article|section|nav|aside|hgroup|header|footer|'.
'figure';
# Regular expression for the content of a block tag.
// Regular expression for the content of a block tag.
$nested_tags_level = 4;
$attr = '
(?> # optional tag attributes
@ -263,7 +363,7 @@ class Markdown implements MarkdownInterface {
|
\'[^\']*\' # text inside single quotes (tolerate ">")
)*
)?
)?
';
$content =
str_repeat('
@ -275,29 +375,32 @@ class Markdown implements MarkdownInterface {
(?>
/>
|
>', $nested_tags_level). # end of opening tag
'.*?'. # last level nested tag content
>', $nested_tags_level). // end of opening tag
'.*?'. // last level nested tag content
str_repeat('
</\2\s*> # closing nested tag
)
|
|
<(?!/\2\s*> # other tags with a different name
)
)*',
$nested_tags_level);
$content2 = str_replace('\2', '\3', $content);
# First, look for nested blocks, e.g.:
# <div>
# <div>
# tags for inner block must be indented.
# </div>
# </div>
#
# The outermost tags must start at the left margin for this to match, and
# the inner nested divs must be indented.
# We need to do this before the next, more liberal match, because the next
# match will start at the first `<div>` and stop at the first `</div>`.
/**
* First, look for nested blocks, e.g.:
* <div>
* <div>
* tags for inner block must be indented.
* </div>
* </div>
*
* The outermost tags must start at the left margin for this to match,
* and the inner nested divs must be indented.
* We need to do this before the next, more liberal match, because the
* next match will start at the first `<div>` and stop at the
* first `</div>`.
*/
$text = preg_replace_callback('{(?>
(?>
(?<=\n) # Starting on its own line
@ -306,9 +409,9 @@ class Markdown implements MarkdownInterface {
)
( # save in $1
# Match from `\n<tag>` to `</tag>\n`, handling nested tags
# Match from `\n<tag>` to `</tag>\n`, handling nested tags
# in between.
[ ]{0,'.$less_than_tab.'}
<('.$block_tags_b_re.')# start tag = $2
'.$attr.'> # attributes followed by > and \n
@ -326,28 +429,28 @@ class Markdown implements MarkdownInterface {
</\3> # the matching end tag
[ ]* # trailing spaces/tabs
(?=\n+|\Z) # followed by a newline or end of document
| # Special case just for <hr />. It was easier to make a special
| # Special case just for <hr />. It was easier to make a special
# case than to make the other regex more complicated.
[ ]{0,'.$less_than_tab.'}
<(hr) # start tag = $2
'.$attr.' # attributes
/?> # the matching end tag
[ ]*
(?=\n{2,}|\Z) # followed by a blank line or end of document
| # Special case for standalone HTML comments:
[ ]{0,'.$less_than_tab.'}
(?s:
<!-- .*? -->
)
[ ]*
(?=\n{2,}|\Z) # followed by a blank line or end of document
| # PHP and ASP-style processor instructions (<? and <%)
[ ]{0,'.$less_than_tab.'}
(?s:
<([?%]) # $2
@ -356,98 +459,118 @@ class Markdown implements MarkdownInterface {
)
[ ]*
(?=\n{2,}|\Z) # followed by a blank line or end of document
)
)}Sxmi',
array($this, '_hashHTMLBlocks_callback'),
$text);
$text
);
return $text;
}
/**
* The callback for hashing HTML blocks
* @param string $matches
* @return string
*/
protected function _hashHTMLBlocks_callback($matches) {
$text = $matches[1];
$key = $this->hashBlock($text);
return "\n\n$key\n\n";
}
/**
* Called whenever a tag must be hashed when a function insert an atomic
* element in the text stream. Passing $text to through this function gives
* a unique text-token which will be reverted back when calling unhash.
*
* The $boundary argument specify what character should be used to surround
* the token. By convension, "B" is used for block elements that needs not
* to be wrapped into paragraph tags at the end, ":" is used for elements
* that are word separators and "X" is used in the general case.
*
* @param string $text
* @param string $boundary
* @return string
*/
protected function hashPart($text, $boundary = 'X') {
#
# Called whenever a tag must be hashed when a function insert an atomic
# element in the text stream. Passing $text to through this function gives
# a unique text-token which will be reverted back when calling unhash.
#
# The $boundary argument specify what character should be used to surround
# the token. By convension, "B" is used for block elements that needs not
# to be wrapped into paragraph tags at the end, ":" is used for elements
# that are word separators and "X" is used in the general case.
#
# Swap back any tag hash found in $text so we do not have to `unhash`
# multiple times at the end.
// Swap back any tag hash found in $text so we do not have to `unhash`
// multiple times at the end.
$text = $this->unhash($text);
# Then hash the block.
// Then hash the block.
static $i = 0;
$key = "$boundary\x1A" . ++$i . $boundary;
$this->html_hashes[$key] = $text;
return $key; # String that will replace the tag.
return $key; // String that will replace the tag.
}
/**
* Shortcut function for hashPart with block-level boundaries.
* @param string $text
* @return string
*/
protected function hashBlock($text) {
#
# Shortcut function for hashPart with block-level boundaries.
#
return $this->hashPart($text, 'B');
}
/**
* Define the block gamut - these are all the transformations that form
* block-level tags like paragraphs, headers, and list items.
* @var array
*/
protected $block_gamut = array(
#
# These are all the transformations that form block-level
# tags like paragraphs, headers, and list items.
#
"doHeaders" => 10,
"doHorizontalRules" => 20,
"doLists" => 40,
"doCodeBlocks" => 50,
"doBlockQuotes" => 60,
);
);
/**
* Run block gamut tranformations.
*
* We need to escape raw HTML in Markdown source before doing anything
* else. This need to be done for each block, and not only at the
* begining in the Markdown function since hashed blocks can be part of
* list items and could have been indented. Indented blocks would have
* been seen as a code block in a previous pass of hashHTMLBlocks.
*
* @param string $text
* @return string
*/
protected function runBlockGamut($text) {
#
# Run block gamut tranformations.
#
# We need to escape raw HTML in Markdown source before doing anything
# else. This need to be done for each block, and not only at the
# begining in the Markdown function since hashed blocks can be part of
# list items and could have been indented. Indented blocks would have
# been seen as a code block in a previous pass of hashHTMLBlocks.
$text = $this->hashHTMLBlocks($text);
return $this->runBasicBlockGamut($text);
}
/**
* Run block gamut tranformations, without hashing HTML blocks. This is
* useful when HTML blocks are known to be already hashed, like in the first
* whole-document pass.
*
* @param string $text
* @return string
*/
protected function runBasicBlockGamut($text) {
#
# Run block gamut tranformations, without hashing HTML blocks. This is
# useful when HTML blocks are known to be already hashed, like in the first
# whole-document pass.
#
foreach ($this->block_gamut as $method => $priority) {
$text = $this->$method($text);
}
# Finally form paragraph and restore hashed blocks.
// Finally form paragraph and restore hashed blocks.
$text = $this->formParagraphs($text);
return $text;
}
/**
* Convert horizontal rules
* @param string $text
* @return string
*/
protected function doHorizontalRules($text) {
# Do Horizontal Rules:
return preg_replace(
'{
^[ ]{0,3} # Leading space
@ -459,67 +582,82 @@ class Markdown implements MarkdownInterface {
[ ]* # Tailing spaces
$ # End of line.
}mx',
"\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n",
$text);
"\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n",
$text
);
}
/**
* These are all the transformations that occur *within* block-level
* tags like paragraphs, headers, and list items.
* @var array
*/
protected $span_gamut = array(
#
# These are all the transformations that occur *within* block-level
# tags like paragraphs, headers, and list items.
#
# Process character escapes, code spans, and inline HTML
# in one shot.
// Process character escapes, code spans, and inline HTML
// in one shot.
"parseSpan" => -30,
# Process anchor and image tags. Images must come first,
# because ![foo][f] looks like an anchor.
// Process anchor and image tags. Images must come first,
// because ![foo][f] looks like an anchor.
"doImages" => 10,
"doAnchors" => 20,
# Make links out of things like `<http://example.com/>`
# Must come after doAnchors, because you can use < and >
# delimiters in inline links like [this](<url>).
// Make links out of things like `<https://example.com/>`
// Must come after doAnchors, because you can use < and >
// delimiters in inline links like [this](<url>).
"doAutoLinks" => 30,
"encodeAmpsAndAngles" => 40,
"doItalicsAndBold" => 50,
"doHardBreaks" => 60,
);
);
/**
* Run span gamut transformations
* @param string $text
* @return string
*/
protected function runSpanGamut($text) {
#
# Run span gamut tranformations.
#
foreach ($this->span_gamut as $method => $priority) {
$text = $this->$method($text);
}
return $text;
}
/**
* Do hard breaks
* @param string $text
* @return string
*/
protected function doHardBreaks($text) {
# Do hard breaks:
return preg_replace_callback('/ {2,}\n/',
array($this, '_doHardBreaks_callback'), $text);
if ($this->hard_wrap) {
return preg_replace_callback('/ *\n/',
array($this, '_doHardBreaks_callback'), $text);
} else {
return preg_replace_callback('/ {2,}\n/',
array($this, '_doHardBreaks_callback'), $text);
}
}
/**
* Trigger part hashing for the hard break (callback method)
* @param array $matches
* @return string
*/
protected function _doHardBreaks_callback($matches) {
return $this->hashPart("<br$this->empty_element_suffix\n");
}
/**
* Turn Markdown link shortcuts into XHTML <a> tags.
* @param string $text
* @return string
*/
protected function doAnchors($text) {
#
# Turn Markdown link shortcuts into XHTML <a> tags.
#
if ($this->in_anchor) return $text;
if ($this->in_anchor) {
return $text;
}
$this->in_anchor = true;
#
# First, handle reference-style links: [link text] [id]
#
// First, handle reference-style links: [link text] [id]
$text = preg_replace_callback('{
( # wrap whole match in $1
\[
@ -536,9 +674,7 @@ class Markdown implements MarkdownInterface {
}xs',
array($this, '_doAnchors_reference_callback'), $text);
#
# Next, inline-style links: [link text](url "optional title")
#
// Next, inline-style links: [link text](url "optional title")
$text = preg_replace_callback('{
( # wrap whole match in $1
\[
@ -563,11 +699,9 @@ class Markdown implements MarkdownInterface {
}xs',
array($this, '_doAnchors_inline_callback'), $text);
#
# Last, handle reference-style shortcuts: [link text]
# These must come last in case you've also got [link text][1]
# or [link text](/foo)
#
// Last, handle reference-style shortcuts: [link text]
// These must come last in case you've also got [link text][1]
// or [link text](/foo)
$text = preg_replace_callback('{
( # wrap whole match in $1
\[
@ -580,48 +714,60 @@ class Markdown implements MarkdownInterface {
$this->in_anchor = false;
return $text;
}
/**
* Callback method to parse referenced anchors
* @param string $matches
* @return string
*/
protected function _doAnchors_reference_callback($matches) {
$whole_match = $matches[1];
$link_text = $matches[2];
$link_id =& $matches[3];
if ($link_id == "") {
# for shortcut links like [this][] or [this].
// for shortcut links like [this][] or [this].
$link_id = $link_text;
}
# lower-case and turn embedded newlines into spaces
// lower-case and turn embedded newlines into spaces
$link_id = strtolower($link_id);
$link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
if (isset($this->urls[$link_id])) {
$url = $this->urls[$link_id];
$url = $this->encodeURLAttribute($url);
$result = "<a href=\"$url\"";
if ( isset( $this->titles[$link_id] ) ) {
$title = $this->titles[$link_id];
$title = $this->encodeAttribute($title);
$result .= " title=\"$title\"";
}
$link_text = $this->runSpanGamut($link_text);
$result .= ">$link_text</a>";
$result = $this->hashPart($result);
}
else {
} else {
$result = $whole_match;
}
return $result;
}
/**
* Callback method to parse inline anchors
* @param string $matches
* @return string
*/
protected function _doAnchors_inline_callback($matches) {
$whole_match = $matches[1];
$link_text = $this->runSpanGamut($matches[2]);
$url = $matches[3] == '' ? $matches[4] : $matches[3];
$title =& $matches[7];
// if the URL was of the form <s p a c e s> it got caught by the HTML
// tag parser and hashed. Need to reverse the process before using the URL.
// If the URL was of the form <s p a c e s> it got caught by the HTML
// tag parser and hashed. Need to reverse the process before using
// the URL.
$unhashed = $this->unhash($url);
if ($unhashed != $url)
$url = preg_replace('/^<(.*)>$/', '\1', $unhashed);
@ -633,21 +779,20 @@ class Markdown implements MarkdownInterface {
$title = $this->encodeAttribute($title);
$result .= " title=\"$title\"";
}
$link_text = $this->runSpanGamut($link_text);
$result .= ">$link_text</a>";
return $this->hashPart($result);
}
/**
* Turn Markdown image shortcuts into <img> tags.
* @param string $text
* @return string
*/
protected function doImages($text) {
#
# Turn Markdown image shortcuts into <img> tags.
#
#
# First, handle reference-style labeled images: ![alt text][id]
#
// First, handle reference-style labeled images: ![alt text][id]
$text = preg_replace_callback('{
( # wrap whole match in $1
!\[
@ -662,13 +807,11 @@ class Markdown implements MarkdownInterface {
\]
)
}xs',
}xs',
array($this, '_doImages_reference_callback'), $text);
#
# Next, handle inline images: ![alt text](url "optional title")
# Don't forget: encode * and _
#
// Next, handle inline images: ![alt text](url "optional title")
// Don't forget: encode * and _
$text = preg_replace_callback('{
( # wrap whole match in $1
!\[
@ -696,13 +839,19 @@ class Markdown implements MarkdownInterface {
return $text;
}
/**
* Callback to parse references image tags
* @param array $matches
* @return string
*/
protected function _doImages_reference_callback($matches) {
$whole_match = $matches[1];
$alt_text = $matches[2];
$link_id = strtolower($matches[3]);
if ($link_id == "") {
$link_id = strtolower($alt_text); # for shortcut links like ![this][].
$link_id = strtolower($alt_text); // for shortcut links like ![this][].
}
$alt_text = $this->encodeAttribute($alt_text);
@ -716,14 +865,19 @@ class Markdown implements MarkdownInterface {
}
$result .= $this->empty_element_suffix;
$result = $this->hashPart($result);
}
else {
# If there's no such link ID, leave intact:
} else {
// If there's no such link ID, leave intact:
$result = $whole_match;
}
return $result;
}
/**
* Callback to parse inline image tags
* @param array $matches
* @return string
*/
protected function _doImages_inline_callback($matches) {
$whole_match = $matches[1];
$alt_text = $matches[2];
@ -735,32 +889,38 @@ class Markdown implements MarkdownInterface {
$result = "<img src=\"$url\" alt=\"$alt_text\"";
if (isset($title)) {
$title = $this->encodeAttribute($title);
$result .= " title=\"$title\""; # $title already quoted
$result .= " title=\"$title\""; // $title already quoted
}
$result .= $this->empty_element_suffix;
return $this->hashPart($result);
}
/**
* Parse Markdown heading elements to HTML
* @param string $text
* @return string
*/
protected function doHeaders($text) {
# Setext-style headers:
# Header 1
# ========
#
# Header 2
# --------
#
/**
* Setext-style headers:
* Header 1
* ========
*
* Header 2
* --------
*/
$text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx',
array($this, '_doHeaders_callback_setext'), $text);
# atx-style headers:
# # Header 1
# ## Header 2
# ## Header 2 with closing hashes ##
# ...
# ###### Header 6
#
/**
* atx-style headers:
* # Header 1
* ## Header 2
* ## Header 2 with closing hashes ##
* ...
* ###### Header 6
*/
$text = preg_replace_callback('{
^(\#{1,6}) # $1 = string of #\'s
[ ]*
@ -773,29 +933,72 @@ class Markdown implements MarkdownInterface {
return $text;
}
/**
* Setext header parsing callback
* @param array $matches
* @return string
*/
protected function _doHeaders_callback_setext($matches) {
# Terrible hack to check we haven't found an empty list item.
if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1]))
// Terrible hack to check we haven't found an empty list item.
if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) {
return $matches[0];
}
$level = $matches[2]{0} == '=' ? 1 : 2;
$block = "<h$level>".$this->runSpanGamut($matches[1])."</h$level>";
// ID attribute generation
$idAtt = $this->_generateIdFromHeaderValue($matches[1]);
$block = "<h$level$idAtt>".$this->runSpanGamut($matches[1])."</h$level>";
return "\n" . $this->hashBlock($block) . "\n\n";
}
/**
* ATX header parsing callback
* @param array $matches
* @return string
*/
protected function _doHeaders_callback_atx($matches) {
// ID attribute generation
$idAtt = $this->_generateIdFromHeaderValue($matches[2]);
$level = strlen($matches[1]);
$block = "<h$level>".$this->runSpanGamut($matches[2])."</h$level>";
$block = "<h$level$idAtt>".$this->runSpanGamut($matches[2])."</h$level>";
return "\n" . $this->hashBlock($block) . "\n\n";
}
/**
* If a header_id_func property is set, we can use it to automatically
* generate an id attribute.
*
* This method returns a string in the form id="foo", or an empty string
* otherwise.
* @param string $headerValue
* @return string
*/
protected function _generateIdFromHeaderValue($headerValue) {
if (!is_callable($this->header_id_func)) {
return "";
}
$idValue = call_user_func($this->header_id_func, $headerValue);
if (!$idValue) {
return "";
}
return ' id="' . $this->encodeAttribute($idValue) . '"';
}
/**
* Form HTML ordered (numbered) and unordered (bulleted) lists.
* @param string $text
* @return string
*/
protected function doLists($text) {
#
# Form HTML ordered (numbered) and unordered (bulleted) lists.
#
$less_than_tab = $this->tab_width - 1;
# Re-usable patterns to match list item bullets and number markers:
// Re-usable patterns to match list item bullets and number markers:
$marker_ul_re = '[*+-]';
$marker_ol_re = '\d+[\.]';
@ -805,7 +1008,7 @@ class Markdown implements MarkdownInterface {
);
foreach ($markers_relist as $marker_re => $other_marker_re) {
# Re-usable pattern to match any entirel ul or ol list:
// Re-usable pattern to match any entirel ul or ol list:
$whole_list_re = '
( # $1 = whole list
( # $2
@ -832,18 +1035,17 @@ class Markdown implements MarkdownInterface {
)
)
'; // mx
# We use a different prefix before nested lists than top-level lists.
# See extended comment in _ProcessListItems().
// We use a different prefix before nested lists than top-level lists.
//See extended comment in _ProcessListItems().
if ($this->list_level) {
$text = preg_replace_callback('{
^
'.$whole_list_re.'
}mx',
array($this, '_doLists_callback'), $text);
}
else {
} else {
$text = preg_replace_callback('{
(?:(?<=\n)\n|\A\n?) # Must eat the newline
'.$whole_list_re.'
@ -854,55 +1056,86 @@ class Markdown implements MarkdownInterface {
return $text;
}
/**
* List parsing callback
* @param array $matches
* @return string
*/
protected function _doLists_callback($matches) {
# Re-usable patterns to match list item bullets and number markers:
// Re-usable patterns to match list item bullets and number markers:
$marker_ul_re = '[*+-]';
$marker_ol_re = '\d+[\.]';
$marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
$marker_ol_start_re = '[0-9]+';
$list = $matches[1];
$list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol";
$marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re );
$list .= "\n";
$result = $this->processListItems($list, $marker_any_re);
$result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>");
$ol_start = 1;
if ($this->enhanced_ordered_list) {
// Get the start number for ordered list.
if ($list_type == 'ol') {
$ol_start_array = array();
$ol_start_check = preg_match("/$marker_ol_start_re/", $matches[4], $ol_start_array);
if ($ol_start_check){
$ol_start = $ol_start_array[0];
}
}
}
if ($ol_start > 1 && $list_type == 'ol'){
$result = $this->hashBlock("<$list_type start=\"$ol_start\">\n" . $result . "</$list_type>");
} else {
$result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>");
}
return "\n". $result ."\n\n";
}
/**
* Nesting tracker for list levels
* @var integer
*/
protected $list_level = 0;
/**
* Process the contents of a single ordered or unordered list, splitting it
* into individual list items.
* @param string $list_str
* @param string $marker_any_re
* @return string
*/
protected function processListItems($list_str, $marker_any_re) {
#
# Process the contents of a single ordered or unordered list, splitting it
# into individual list items.
#
# The $this->list_level global keeps track of when we're inside a list.
# Each time we enter a list, we increment it; when we leave a list,
# we decrement. If it's zero, we're not in a list anymore.
#
# We do this because when we're not inside a list, we want to treat
# something like this:
#
# I recommend upgrading to version
# 8. Oops, now this line is treated
# as a sub-list.
#
# As a single paragraph, despite the fact that the second line starts
# with a digit-period-space sequence.
#
# Whereas when we're inside a list (or sub-list), that line will be
# treated as the start of a sub-list. What a kludge, huh? This is
# an aspect of Markdown's syntax that's hard to parse perfectly
# without resorting to mind-reading. Perhaps the solution is to
# change the syntax rules such that sub-lists must start with a
# starting cardinal number; e.g. "1." or "a.".
/**
* The $this->list_level global keeps track of when we're inside a list.
* Each time we enter a list, we increment it; when we leave a list,
* we decrement. If it's zero, we're not in a list anymore.
*
* We do this because when we're not inside a list, we want to treat
* something like this:
*
* I recommend upgrading to version
* 8. Oops, now this line is treated
* as a sub-list.
*
* As a single paragraph, despite the fact that the second line starts
* with a digit-period-space sequence.
*
* Whereas when we're inside a list (or sub-list), that line will be
* treated as the start of a sub-list. What a kludge, huh? This is
* an aspect of Markdown's syntax that's hard to parse perfectly
* without resorting to mind-reading. Perhaps the solution is to
* change the syntax rules such that sub-lists must start with a
* starting cardinal number; e.g. "1." or "a.".
*/
$this->list_level++;
# trim trailing blank lines:
// Trim trailing blank lines:
$list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
$list_str = preg_replace_callback('{
@ -920,6 +1153,12 @@ class Markdown implements MarkdownInterface {
$this->list_level--;
return $list_str;
}
/**
* List item parsing callback
* @param array $matches
* @return string
*/
protected function _processListItems_callback($matches) {
$item = $matches[4];
$leading_line =& $matches[1];
@ -927,28 +1166,27 @@ class Markdown implements MarkdownInterface {
$marker_space = $matches[3];
$tailing_blank_line =& $matches[5];
if ($leading_line || $tailing_blank_line ||
if ($leading_line || $tailing_blank_line ||
preg_match('/\n{2,}/', $item))
{
# Replace marker with the appropriate whitespace indentation
// Replace marker with the appropriate whitespace indentation
$item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item;
$item = $this->runBlockGamut($this->outdent($item)."\n");
}
else {
# Recursion for sub-lists:
} else {
// Recursion for sub-lists:
$item = $this->doLists($this->outdent($item));
$item = preg_replace('/\n+$/', '', $item);
$item = $this->runSpanGamut($item);
$item = $this->formParagraphs($item, false);
}
return "<li>" . $item . "</li>\n";
}
/**
* Process Markdown `<pre><code>` blocks.
* @param string $text
* @return string
*/
protected function doCodeBlocks($text) {
#
# Process Markdown `<pre><code>` blocks.
#
$text = preg_replace_callback('{
(?:\n\n|\A\n?)
( # $1 = the code block -- one or more lines, starting with a space/tab
@ -963,106 +1201,141 @@ class Markdown implements MarkdownInterface {
return $text;
}
/**
* Code block parsing callback
* @param array $matches
* @return string
*/
protected function _doCodeBlocks_callback($matches) {
$codeblock = $matches[1];
$codeblock = $this->outdent($codeblock);
$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
if ($this->code_block_content_func) {
$codeblock = call_user_func($this->code_block_content_func, $codeblock, "");
} else {
$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
}
# trim leading newlines and trailing newlines
$codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
$codeblock = "<pre><code>$codeblock\n</code></pre>";
return "\n\n".$this->hashBlock($codeblock)."\n\n";
return "\n\n" . $this->hashBlock($codeblock) . "\n\n";
}
/**
* Create a code span markup for $code. Called from handleSpanToken.
* @param string $code
* @return string
*/
protected function makeCodeSpan($code) {
#
# Create a code span markup for $code. Called from handleSpanToken.
#
$code = htmlspecialchars(trim($code), ENT_NOQUOTES);
if ($this->code_span_content_func) {
$code = call_user_func($this->code_span_content_func, $code);
} else {
$code = htmlspecialchars(trim($code), ENT_NOQUOTES);
}
return $this->hashPart("<code>$code</code>");
}
/**
* Define the emphasis operators with their regex matches
* @var array
*/
protected $em_relist = array(
'' => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?![\.,:;]?\s)',
'*' => '(?<![\s*])\*(?!\*)',
'_' => '(?<![\s_])_(?!_)',
);
);
/**
* Define the strong operators with their regex matches
* @var array
*/
protected $strong_relist = array(
'' => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?![\.,:;]?\s)',
'**' => '(?<![\s*])\*\*(?!\*)',
'__' => '(?<![\s_])__(?!_)',
);
);
/**
* Define the emphasis + strong operators with their regex matches
* @var array
*/
protected $em_strong_relist = array(
'' => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?![\.,:;]?\s)',
'***' => '(?<![\s*])\*\*\*(?!\*)',
'___' => '(?<![\s_])___(?!_)',
);
);
/**
* Container for prepared regular expressions
* @var array
*/
protected $em_strong_prepared_relist;
/**
* Prepare regular expressions for searching emphasis tokens in any
* context.
* @return void
*/
protected function prepareItalicsAndBold() {
#
# Prepare regular expressions for searching emphasis tokens in any
# context.
#
foreach ($this->em_relist as $em => $em_re) {
foreach ($this->strong_relist as $strong => $strong_re) {
# Construct list of allowed token expressions.
// Construct list of allowed token expressions.
$token_relist = array();
if (isset($this->em_strong_relist["$em$strong"])) {
$token_relist[] = $this->em_strong_relist["$em$strong"];
}
$token_relist[] = $em_re;
$token_relist[] = $strong_re;
# Construct master expression from list.
$token_re = '{('. implode('|', $token_relist) .')}';
// Construct master expression from list.
$token_re = '{(' . implode('|', $token_relist) . ')}';
$this->em_strong_prepared_relist["$em$strong"] = $token_re;
}
}
}
/**
* Convert Markdown italics (emphasis) and bold (strong) to HTML
* @param string $text
* @return string
*/
protected function doItalicsAndBold($text) {
$token_stack = array('');
$text_stack = array('');
$em = '';
$strong = '';
$tree_char_em = false;
while (1) {
#
# Get prepared regular expression for seraching emphasis tokens
# in current context.
#
// Get prepared regular expression for seraching emphasis tokens
// in current context.
$token_re = $this->em_strong_prepared_relist["$em$strong"];
#
# Each loop iteration search for the next emphasis token.
# Each token is then passed to handleSpanToken.
#
// Each loop iteration search for the next emphasis token.
// Each token is then passed to handleSpanToken.
$parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
$text_stack[0] .= $parts[0];
$token =& $parts[1];
$text =& $parts[2];
if (empty($token)) {
# Reached end of text span: empty stack without emitting.
# any more emphasis.
// Reached end of text span: empty stack without emitting.
// any more emphasis.
while ($token_stack[0]) {
$text_stack[1] .= array_shift($token_stack);
$text_stack[0] .= array_shift($text_stack);
}
break;
}
$token_len = strlen($token);
if ($tree_char_em) {
# Reached closing marker while inside a three-char emphasis.
// Reached closing marker while inside a three-char emphasis.
if ($token_len == 3) {
# Three-char closing marker, close em and strong.
// Three-char closing marker, close em and strong.
array_shift($token_stack);
$span = array_shift($text_stack);
$span = $this->runSpanGamut($span);
@ -1071,21 +1344,21 @@ class Markdown implements MarkdownInterface {
$em = '';
$strong = '';
} else {
# Other closing marker: close one em or strong and
# change current token state to match the other
// Other closing marker: close one em or strong and
// change current token state to match the other
$token_stack[0] = str_repeat($token{0}, 3-$token_len);
$tag = $token_len == 2 ? "strong" : "em";
$span = $text_stack[0];
$span = $this->runSpanGamut($span);
$span = "<$tag>$span</$tag>";
$text_stack[0] = $this->hashPart($span);
$$tag = ''; # $$tag stands for $em or $strong
$$tag = ''; // $$tag stands for $em or $strong
}
$tree_char_em = false;
} else if ($token_len == 3) {
if ($em) {
# Reached closing marker for both em and strong.
# Closing strong marker:
// Reached closing marker for both em and strong.
// Closing strong marker:
for ($i = 0; $i < 2; ++$i) {
$shifted_token = array_shift($token_stack);
$tag = strlen($shifted_token) == 2 ? "strong" : "em";
@ -1093,11 +1366,11 @@ class Markdown implements MarkdownInterface {
$span = $this->runSpanGamut($span);
$span = "<$tag>$span</$tag>";
$text_stack[0] .= $this->hashPart($span);
$$tag = ''; # $$tag stands for $em or $strong
$$tag = ''; // $$tag stands for $em or $strong
}
} else {
# Reached opening three-char emphasis marker. Push on token
# stack; will be handled by the special condition above.
// Reached opening three-char emphasis marker. Push on token
// stack; will be handled by the special condition above.
$em = $token{0};
$strong = "$em$em";
array_unshift($token_stack, $token);
@ -1106,12 +1379,12 @@ class Markdown implements MarkdownInterface {
}
} else if ($token_len == 2) {
if ($strong) {
# Unwind any dangling emphasis marker:
// Unwind any dangling emphasis marker:
if (strlen($token_stack[0]) == 1) {
$text_stack[1] .= array_shift($token_stack);
$text_stack[0] .= array_shift($text_stack);
}
# Closing strong marker:
// Closing strong marker:
array_shift($token_stack);
$span = array_shift($text_stack);
$span = $this->runSpanGamut($span);
@ -1124,10 +1397,10 @@ class Markdown implements MarkdownInterface {
$strong = $token;
}
} else {
# Here $token_len == 1
// Here $token_len == 1
if ($em) {
if (strlen($token_stack[0]) == 1) {
# Closing emphasis marker:
// Closing emphasis marker:
array_shift($token_stack);
$span = array_shift($text_stack);
$span = $this->runSpanGamut($span);
@ -1147,7 +1420,11 @@ class Markdown implements MarkdownInterface {
return $text_stack[0];
}
/**
* Parse Markdown blockquotes to HTML
* @param string $text
* @return string
*/
protected function doBlockQuotes($text) {
$text = preg_replace_callback('/
( # Wrap whole match in $1
@ -1163,51 +1440,64 @@ class Markdown implements MarkdownInterface {
return $text;
}
/**
* Blockquote parsing callback
* @param array $matches
* @return string
*/
protected function _doBlockQuotes_callback($matches) {
$bq = $matches[1];
# trim one level of quoting - trim whitespace-only lines
// trim one level of quoting - trim whitespace-only lines
$bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq);
$bq = $this->runBlockGamut($bq); # recurse
$bq = $this->runBlockGamut($bq); // recurse
$bq = preg_replace('/^/m', " ", $bq);
# These leading spaces cause problem with <pre> content,
# so we need to fix that:
$bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx',
// These leading spaces cause problem with <pre> content,
// so we need to fix that:
$bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx',
array($this, '_doBlockQuotes_callback2'), $bq);
return "\n". $this->hashBlock("<blockquote>\n$bq\n</blockquote>")."\n\n";
return "\n" . $this->hashBlock("<blockquote>\n$bq\n</blockquote>") . "\n\n";
}
/**
* Blockquote parsing callback
* @param array $matches
* @return string
*/
protected function _doBlockQuotes_callback2($matches) {
$pre = $matches[1];
$pre = preg_replace('/^ /m', '', $pre);
return $pre;
}
protected function formParagraphs($text) {
#
# Params:
# $text - string to process with html <p> tags
#
# Strip leading and trailing lines:
/**
* Parse paragraphs
*
* @param string $text String to process in paragraphs
* @param boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags
* @return string
*/
protected function formParagraphs($text, $wrap_in_p = true) {
// Strip leading and trailing lines:
$text = preg_replace('/\A\n+|\n+\z/', '', $text);
$grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
#
# Wrap <p> tags and unhashify HTML blocks
#
// Wrap <p> tags and unhashify HTML blocks
foreach ($grafs as $key => $value) {
if (!preg_match('/^B\x1A[0-9]+B$/', $value)) {
# Is a paragraph.
// Is a paragraph.
$value = $this->runSpanGamut($value);
$value = preg_replace('/^([ ]*)/', "<p>", $value);
$value .= "</p>";
if ($wrap_in_p) {
$value = preg_replace('/^([ ]*)/', "<p>", $value);
$value .= "</p>";
}
$grafs[$key] = $this->unhash($value);
}
else {
# Is a block.
# Modify elements of @grafs in-place...
} else {
// Is a block.
// Modify elements of @grafs in-place...
$graf = $value;
$block = $this->html_hashes[$graf];
$graf = $block;
@ -1232,11 +1522,11 @@ class Markdown implements MarkdownInterface {
// {
// list(, $div_open, , $div_content, $div_close) = $matches;
//
// # We can't call Markdown(), because that resets the hash;
// # that initialization code should be pulled into its own sub, though.
// // We can't call Markdown(), because that resets the hash;
// // that initialization code should be pulled into its own sub, though.
// $div_content = $this->hashHTMLBlocks($div_content);
//
// # Run document gamut methods on the content.
//
// // Run document gamut methods on the content.
// foreach ($this->document_gamut as $method => $priority) {
// $div_content = $this->$method($div_content);
// }
@ -1253,71 +1543,78 @@ class Markdown implements MarkdownInterface {
return implode("\n\n", $grafs);
}
/**
* Encode text for a double-quoted HTML attribute. This function
* is *not* suitable for attributes enclosed in single quotes.
* @param string $text
* @return string
*/
protected function encodeAttribute($text) {
#
# Encode text for a double-quoted HTML attribute. This function
# is *not* suitable for attributes enclosed in single quotes.
#
$text = $this->encodeAmpsAndAngles($text);
$text = str_replace('"', '&quot;', $text);
return $text;
}
/**
* Encode text for a double-quoted HTML attribute containing a URL,
* applying the URL filter if set. Also generates the textual
* representation for the URL (removing mailto: or tel:) storing it in $text.
* This function is *not* suitable for attributes enclosed in single quotes.
*
* @param string $url
* @param string &$text Passed by reference
* @return string URL
*/
protected function encodeURLAttribute($url, &$text = null) {
#
# Encode text for a double-quoted HTML attribute containing a URL,
# applying the URL filter if set. Also generates the textual
# representation for the URL (removing mailto: or tel:) storing it in $text.
# This function is *not* suitable for attributes enclosed in single quotes.
#
if ($this->url_filter_func)
if ($this->url_filter_func) {
$url = call_user_func($this->url_filter_func, $url);
}
if (preg_match('{^mailto:}i', $url))
if (preg_match('{^mailto:}i', $url)) {
$url = $this->encodeEntityObfuscatedAttribute($url, $text, 7);
else if (preg_match('{^tel:}i', $url))
{
} else if (preg_match('{^tel:}i', $url)) {
$url = $this->encodeAttribute($url);
$text = substr($url, 4);
}
else
{
} else {
$url = $this->encodeAttribute($url);
$text = $url;
}
return $url;
}
/**
* Smart processing for ampersands and angle brackets that need to
* be encoded. Valid character entities are left alone unless the
* no-entities mode is set.
* @param string $text
* @return string
*/
protected function encodeAmpsAndAngles($text) {
#
# Smart processing for ampersands and angle brackets that need to
# be encoded. Valid character entities are left alone unless the
# no-entities mode is set.
#
if ($this->no_entities) {
$text = str_replace('&', '&amp;', $text);
} else {
# Ampersand-encoding based entirely on Nat Irons's Amputator
# MT plugin: <http://bumppo.net/projects/amputator/>
$text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/',
// Ampersand-encoding based entirely on Nat Irons's Amputator
// MT plugin: <http://bumppo.net/projects/amputator/>
$text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/',
'&amp;', $text);
}
# Encode remaining <'s
// Encode remaining <'s
$text = str_replace('<', '&lt;', $text);
return $text;
}
/**
* Parse Markdown automatic links to anchor HTML tags
* @param string $text
* @return string
*/
protected function doAutoLinks($text) {
$text = preg_replace_callback('{<((https?|ftp|dict|tel):[^\'">\s]+)>}i',
array($this, '_doAutoLinks_url_callback'), $text);
# Email addresses: <address@domain.foo>
// Email addresses: <address@domain.foo>
$text = preg_replace_callback('{
<
(?:mailto:)?
@ -1340,11 +1637,23 @@ class Markdown implements MarkdownInterface {
return $text;
}
/**
* Parse URL callback
* @param array $matches
* @return string
*/
protected function _doAutoLinks_url_callback($matches) {
$url = $this->encodeURLAttribute($matches[1], $text);
$link = "<a href=\"$url\">$text</a>";
return $this->hashPart($link);
}
/**
* Parse email address callback
* @param array $matches
* @return string
*/
protected function _doAutoLinks_email_callback($matches) {
$addr = $matches[1];
$url = $this->encodeURLAttribute("mailto:$addr", $text);
@ -1352,42 +1661,52 @@ class Markdown implements MarkdownInterface {
return $this->hashPart($link);
}
/**
* Input: some text to obfuscate, e.g. "mailto:foo@example.com"
*
* Output: the same text but with most characters encoded as either a
* decimal or hex entity, in the hopes of foiling most address
* harvesting spam bots. E.g.:
*
* &#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111;
* &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111;
* &#x6d;
*
* Note: the additional output $tail is assigned the same value as the
* ouput, minus the number of characters specified by $head_length.
*
* Based by a filter by Matthew Wickline, posted to BBEdit-Talk.
* With some optimizations by Milian Wolff. Forced encoding of HTML
* attribute special characters by Allan Odgaard.
*
* @param string $text
* @param string &$tail
* @param integer $head_length
* @return string
*/
protected function encodeEntityObfuscatedAttribute($text, &$tail = null, $head_length = 0) {
#
# Input: some text to obfuscate, e.g. "mailto:foo@example.com"
#
# Output: the same text but with most characters encoded as either a
# decimal or hex entity, in the hopes of foiling most address
# harvesting spam bots. E.g.:
#
# &#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111;
# &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111;
# &#x6d;
#
# Note: the additional output $tail is assigned the same value as the
# ouput, minus the number of characters specified by $head_length.
#
# Based by a filter by Matthew Wickline, posted to BBEdit-Talk.
# With some optimizations by Milian Wolff. Forced encoding of HTML
# attribute special characters by Allan Odgaard.
#
if ($text == "") return $tail = "";
if ($text == "") {
return $tail = "";
}
$chars = preg_split('/(?<!^)(?!$)/', $text);
$seed = (int)abs(crc32($text) / strlen($text)); # Deterministic seed.
$seed = (int)abs(crc32($text) / strlen($text)); // Deterministic seed.
foreach ($chars as $key => $char) {
$ord = ord($char);
# Ignore non-ascii chars.
// Ignore non-ascii chars.
if ($ord < 128) {
$r = ($seed * (1 + $key)) % 100; # Pseudo-random function.
# roughly 10% raw, 45% hex, 45% dec
# '@' *must* be encoded. I insist.
# '"' and '>' have to be encoded inside the attribute
if ($r > 90 && strpos('@"&>', $char) === false) /* do nothing */;
else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';';
else $chars[$key] = '&#'.$ord.';';
$r = ($seed * (1 + $key)) % 100; // Pseudo-random function.
// roughly 10% raw, 45% hex, 45% dec
// '@' *must* be encoded. I insist.
// '"' and '>' have to be encoded inside the attribute
if ($r > 90 && strpos('@"&>', $char) === false) {
/* do nothing */
} else if ($r < 45) {
$chars[$key] = '&#x'.dechex($ord).';';
} else {
$chars[$key] = '&#'.$ord.';';
}
}
}
@ -1397,14 +1716,15 @@ class Markdown implements MarkdownInterface {
return $text;
}
/**
* Take the string $str and parse it into tokens, hashing embeded HTML,
* escaped characters and handling code spans.
* @param string $str
* @return string
*/
protected function parseSpan($str) {
#
# Take the string $str and parse it into tokens, hashing embeded HTML,
# escaped characters and handling code spans.
#
$output = '';
$span_re = '{
(
\\\\'.$this->escape_chars_re.'
@ -1432,1720 +1752,145 @@ class Markdown implements MarkdownInterface {
}xs';
while (1) {
#
# Each loop iteration seach for either the next tag, the next
# openning code span marker, or the next escaped character.
# Each token is then passed to handleSpanToken.
#
// Each loop iteration seach for either the next tag, the next
// openning code span marker, or the next escaped character.
// Each token is then passed to handleSpanToken.
$parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE);
# Create token from text preceding tag.
// Create token from text preceding tag.
if ($parts[0] != "") {
$output .= $parts[0];
}
# Check if we reach the end.
// Check if we reach the end.
if (isset($parts[1])) {
$output .= $this->handleSpanToken($parts[1], $parts[2]);
$str = $parts[2];
}
else {
} else {
break;
}
}
return $output;
}
/**
* Handle $token provided by parseSpan by determining its nature and
* returning the corresponding value that should replace it.
* @param string $token
* @param string &$str
* @return string
*/
protected function handleSpanToken($token, &$str) {
#
# Handle $token provided by parseSpan by determining its nature and
# returning the corresponding value that should replace it.
#
switch ($token{0}) {
case "\\":
return $this->hashPart("&#". ord($token{1}). ";");
case "`":
# Search for end marker in remaining text.
if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm',
// Search for end marker in remaining text.
if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm',
$str, $matches))
{
$str = $matches[2];
$codespan = $this->makeCodeSpan($matches[1]);
return $this->hashPart($codespan);
}
return $token; // return as text since no ending marker found.
return $token; // Return as text since no ending marker found.
default:
return $this->hashPart($token);
}
}
/**
* Remove one level of line-leading tabs or spaces
* @param string $text
* @return string
*/
protected function outdent($text) {
#
# Remove one level of line-leading tabs or spaces
#
return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text);
return preg_replace('/^(\t|[ ]{1,' . $this->tab_width . '})/m', '', $text);
}
# String length function for detab. `_initDetab` will create a function to
# hanlde UTF-8 if the default function does not exist.
/**
* String length function for detab. `_initDetab` will create a function to
* handle UTF-8 if the default function does not exist.
* @var string
*/
protected $utf8_strlen = 'mb_strlen';
/**
* Replace tabs with the appropriate amount of spaces.
*
* For each line we separate the line in blocks delemited by tab characters.
* Then we reconstruct every line by adding the appropriate number of space
* between each blocks.
*
* @param string $text
* @return string
*/
protected function detab($text) {
#
# Replace tabs with the appropriate amount of space.
#
# For each line we separate the line in blocks delemited by
# tab characters. Then we reconstruct every line by adding the
# appropriate number of space between each blocks.
$text = preg_replace_callback('/^.*\t.*$/m',
array($this, '_detab_callback'), $text);
return $text;
}
/**
* Replace tabs callback
* @param string $matches
* @return string
*/
protected function _detab_callback($matches) {
$line = $matches[0];
$strlen = $this->utf8_strlen; # strlen function for UTF-8.
# Split in blocks.
$strlen = $this->utf8_strlen; // strlen function for UTF-8.
// Split in blocks.
$blocks = explode("\t", $line);
# Add each blocks to the line.
// Add each blocks to the line.
$line = $blocks[0];
unset($blocks[0]); # Do not add first block twice.
unset($blocks[0]); // Do not add first block twice.
foreach ($blocks as $block) {
# Calculate amount of space, insert spaces, insert block.
$amount = $this->tab_width -
// Calculate amount of space, insert spaces, insert block.
$amount = $this->tab_width -
$strlen($line, 'UTF-8') % $this->tab_width;
$line .= str_repeat(" ", $amount) . $block;
}
return $line;
}
/**
* Check for the availability of the function in the `utf8_strlen` property
* (initially `mb_strlen`). If the function is not available, create a
* function that will loosely count the number of UTF-8 characters with a
* regular expression.
* @return void
*/
protected function _initDetab() {
#
# Check for the availability of the function in the `utf8_strlen` property
# (initially `mb_strlen`). If the function is not available, create a
# function that will loosely count the number of UTF-8 characters with a
# regular expression.
#
if (function_exists($this->utf8_strlen)) return;
if (function_exists($this->utf8_strlen)) {
return;
}
$this->utf8_strlen = create_function('$text', 'return preg_match_all(
"/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/",
"/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/",
$text, $m);');
}
/**
* Swap back in all the tags hashed by _HashHTMLBlocks.
* @param string $text
* @return string
*/
protected function unhash($text) {
#
# Swap back in all the tags hashed by _HashHTMLBlocks.
#
return preg_replace_callback('/(.)\x1A[0-9]+\1/',
return preg_replace_callback('/(.)\x1A[0-9]+\1/',
array($this, '_unhash_callback'), $text);
}
/**
* Unhashing callback
* @param array $matches
* @return string
*/
protected function _unhash_callback($matches) {
return $this->html_hashes[$matches[0]];
}
}
#
# Temporary Markdown Extra Parser Implementation Class
#
# NOTE: DON'T USE THIS CLASS
# Currently the implementation of of Extra resides here in this temporary class.
# This makes it easier to propagate the changes between the three different
# packaging styles of PHP Markdown. When this issue is resolved, this
# MarkdownExtra_TmpImpl class here will disappear and \Michelf\MarkdownExtra
# will contain the code. So please use \Michelf\MarkdownExtra and ignore this
# one.
#
abstract class _MarkdownExtra_TmpImpl extends \Michelf\Markdown {
### Configuration Variables ###
# Prefix for footnote ids.
public $fn_id_prefix = "";
# Optional title attribute for footnote links and backlinks.
public $fn_link_title = "";
public $fn_backlink_title = "";
# Optional class attribute for footnote links and backlinks.
public $fn_link_class = "footnote-ref";
public $fn_backlink_class = "footnote-backref";
# Class name for table cell alignment (%% replaced left/center/right)
# For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center'
# If empty, the align attribute is used instead of a class name.
public $table_align_class_tmpl = '';
# Optional class prefix for fenced code block.
public $code_class_prefix = "";
# Class attribute for code blocks goes on the `code` tag;
# setting this to true will put attributes on the `pre` tag instead.
public $code_attr_on_pre = false;
# Predefined abbreviations.
public $predef_abbr = array();
### Parser Implementation ###
public function __construct() {
#
# Constructor function. Initialize the parser object.
#
# Add extra escapable characters before parent constructor
# initialize the table.
$this->escape_chars .= ':|';
# Insert extra document, block, and span transformations.
# Parent constructor will do the sorting.
$this->document_gamut += array(
"doFencedCodeBlocks" => 5,
"stripFootnotes" => 15,
"stripAbbreviations" => 25,
"appendFootnotes" => 50,
);
$this->block_gamut += array(
"doFencedCodeBlocks" => 5,
"doTables" => 15,
"doDefLists" => 45,
);
$this->span_gamut += array(
"doFootnotes" => 5,
"doAbbreviations" => 70,
);
parent::__construct();
}
# Extra variables used during extra transformations.
protected $footnotes = array();
protected $footnotes_ordered = array();
protected $footnotes_ref_count = array();
protected $footnotes_numbers = array();
protected $abbr_desciptions = array();
protected $abbr_word_re = '';
# Give the current footnote number.
protected $footnote_counter = 1;
protected function setup() {
#
# Setting up Extra-specific variables.
#
parent::setup();
$this->footnotes = array();
$this->footnotes_ordered = array();
$this->footnotes_ref_count = array();
$this->footnotes_numbers = array();
$this->abbr_desciptions = array();
$this->abbr_word_re = '';
$this->footnote_counter = 1;
foreach ($this->predef_abbr as $abbr_word => $abbr_desc) {
if ($this->abbr_word_re)
$this->abbr_word_re .= '|';
$this->abbr_word_re .= preg_quote($abbr_word);
$this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
}
}
protected function teardown() {
#
# Clearing Extra-specific variables.
#
$this->footnotes = array();
$this->footnotes_ordered = array();
$this->footnotes_ref_count = array();
$this->footnotes_numbers = array();
$this->abbr_desciptions = array();
$this->abbr_word_re = '';
parent::teardown();
}
### Extra Attribute Parser ###
# Expression to use to catch attributes (includes the braces)
protected $id_class_attr_catch_re = '\{((?:[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,})[ ]*\}';
# Expression to use when parsing in a context when no capture is desired
protected $id_class_attr_nocatch_re = '\{(?:[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,}[ ]*\}';
protected function doExtraAttributes($tag_name, $attr) {
#
# Parse attributes caught by the $this->id_class_attr_catch_re expression
# and return the HTML-formatted list of attributes.
#
# Currently supported attributes are .class and #id.
#
if (empty($attr)) return "";
# Split on components
preg_match_all('/[#.a-z][-_:a-zA-Z0-9=]+/', $attr, $matches);
$elements = $matches[0];
# handle classes and ids (only first id taken into account)
$classes = array();
$attributes = array();
$id = false;
foreach ($elements as $element) {
if ($element{0} == '.') {
$classes[] = substr($element, 1);
} else if ($element{0} == '#') {
if ($id === false) $id = substr($element, 1);
} else if (strpos($element, '=') > 0) {
$parts = explode('=', $element, 2);
$attributes[] = $parts[0] . '="' . $parts[1] . '"';
}
}
# compose attributes as string
$attr_str = "";
if (!empty($id)) {
$attr_str .= ' id="'.$id.'"';
}
if (!empty($classes)) {
$attr_str .= ' class="'.implode(" ", $classes).'"';
}
if (!$this->no_markup && !empty($attributes)) {
$attr_str .= ' '.implode(" ", $attributes);
}
return $attr_str;
}
protected function stripLinkDefinitions($text) {
#
# Strips link definitions from text, stores the URLs and titles in
# hash references.
#
$less_than_tab = $this->tab_width - 1;
# Link defs are in the form: ^[id]: url "optional title"
$text = preg_replace_callback('{
^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1
[ ]*
\n? # maybe *one* newline
[ ]*
(?:
<(.+?)> # url = $2
|
(\S+?) # url = $3
)
[ ]*
\n? # maybe one newline
[ ]*
(?:
(?<=\s) # lookbehind for whitespace
["(]
(.*?) # title = $4
[")]
[ ]*
)? # title is optional
(?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr
(?:\n+|\Z)
}xm',
array($this, '_stripLinkDefinitions_callback'),
$text);
return $text;
}
protected function _stripLinkDefinitions_callback($matches) {
$link_id = strtolower($matches[1]);
$url = $matches[2] == '' ? $matches[3] : $matches[2];
$this->urls[$link_id] = $url;
$this->titles[$link_id] =& $matches[4];
$this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]);
return ''; # String that will replace the block
}
### HTML Block Parser ###
# Tags that are always treated as block tags:
protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|figure';
# Tags treated as block tags only if the opening tag is alone on its line:
protected $context_block_tags_re = 'script|noscript|style|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video';
# Tags where markdown="1" default to span mode:
protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address';
# Tags which must not have their contents modified, no matter where
# they appear:
protected $clean_tags_re = 'script|style|math|svg';
# Tags that do not need to be closed.
protected $auto_close_tags_re = 'hr|img|param|source|track';
protected function hashHTMLBlocks($text) {
#
# Hashify HTML Blocks and "clean tags".
#
# We only want to do this for block-level HTML tags, such as headers,
# lists, and tables. That's because we still want to wrap <p>s around
# "paragraphs" that are wrapped in non-block-level tags, such as anchors,
# phrase emphasis, and spans. The list of tags we're looking for is
# hard-coded.
#
# This works by calling _HashHTMLBlocks_InMarkdown, which then calls
# _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1"
# attribute is found within a tag, _HashHTMLBlocks_InHTML calls back
# _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
# These two functions are calling each other. It's recursive!
#
if ($this->no_markup) return $text;
#
# Call the HTML-in-Markdown hasher.
#
list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text);
return $text;
}
protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0,
$enclosing_tag_re = '', $span = false)
{
#
# Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags.
#
# * $indent is the number of space to be ignored when checking for code
# blocks. This is important because if we don't take the indent into
# account, something like this (which looks right) won't work as expected:
#
# <div>
# <div markdown="1">
# Hello World. <-- Is this a Markdown code block or text?
# </div> <-- Is this a Markdown code block or a real tag?
# <div>
#
# If you don't like this, just don't indent the tag on which
# you apply the markdown="1" attribute.
#
# * If $enclosing_tag_re is not empty, stops at the first unmatched closing
# tag with that name. Nested tags supported.
#
# * If $span is true, text inside must treated as span. So any double
# newline will be replaced by a single newline so that it does not create
# paragraphs.
#
# Returns an array of that form: ( processed text , remaining text )
#
if ($text === '') return array('', '');
# Regex to check for the presense of newlines around a block tag.
$newline_before_re = '/(?:^\n?|\n\n)*$/';
$newline_after_re =
'{
^ # Start of text following the tag.
(?>[ ]*<!--.*?-->)? # Optional comment.
[ ]*\n # Must be followed by newline.
}xs';
# Regex to match any tag.
$block_tag_re =
'{
( # $2: Capture whole tag.
</? # Any opening or closing tag.
(?> # Tag name.
'.$this->block_tags_re.' |
'.$this->context_block_tags_re.' |
'.$this->clean_tags_re.' |
(?!\s)'.$enclosing_tag_re.'
)
(?:
(?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
(?>
".*?" | # Double quotes (can contain `>`)
\'.*?\' | # Single quotes (can contain `>`)
.+? # Anything but quotes and `>`.
)*?
)?
> # End of tag.
|
<!-- .*? --> # HTML Comment
|
<\?.*?\?> | <%.*?%> # Processing instruction
|
<!\[CDATA\[.*?\]\]> # CData Block
'. ( !$span ? ' # If not in span.
|
# Indented code block
(?: ^[ ]*\n | ^ | \n[ ]*\n )
[ ]{'.($indent+4).'}[^\n]* \n
(?>
(?: [ ]{'.($indent+4).'}[^\n]* | [ ]* ) \n
)*
|
# Fenced code block marker
(?<= ^ | \n )
[ ]{0,'.($indent+3).'}(?:~{3,}|`{3,})
[ ]*
(?:
\.?[-_:a-zA-Z0-9]+ # standalone class name
|
'.$this->id_class_attr_nocatch_re.' # extra attributes
)?
[ ]*
(?= \n )
' : '' ). ' # End (if not is span).
|
# Code span marker
# Note, this regex needs to go after backtick fenced
# code blocks but it should also be kept outside of the
# "if not in span" condition adding backticks to the parser
`+
)
}xs';
$depth = 0; # Current depth inside the tag tree.
$parsed = ""; # Parsed text that will be returned.
#
# Loop through every tag until we find the closing tag of the parent
# or loop until reaching the end of text if no parent tag specified.
#
do {
#
# Split the text using the first $tag_match pattern found.
# Text before pattern will be first in the array, text after
# pattern will be at the end, and between will be any catches made
# by the pattern.
#
$parts = preg_split($block_tag_re, $text, 2,
PREG_SPLIT_DELIM_CAPTURE);
# If in Markdown span mode, add a empty-string span-level hash
# after each newline to prevent triggering any block element.
if ($span) {
$void = $this->hashPart("", ':');
$newline = "$void\n";
$parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void;
}
$parsed .= $parts[0]; # Text before current tag.
# If end of $text has been reached. Stop loop.
if (count($parts) < 3) {
$text = "";
break;
}
$tag = $parts[1]; # Tag to handle.
$text = $parts[2]; # Remaining text after current tag.
$tag_re = preg_quote($tag); # For use in a regular expression.
#
# Check for: Fenced code block marker.
# Note: need to recheck the whole tag to disambiguate backtick
# fences from code spans
#
if (preg_match('{^\n?([ ]{0,'.($indent+3).'})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+|'.$this->id_class_attr_nocatch_re.')?[ ]*\n?$}', $tag, $capture)) {
# Fenced code block marker: find matching end marker.
$fence_indent = strlen($capture[1]); # use captured indent in re
$fence_re = $capture[2]; # use captured fence in re
if (preg_match('{^(?>.*\n)*?[ ]{'.($fence_indent).'}'.$fence_re.'[ ]*(?:\n|$)}', $text,
$matches))
{
# End marker found: pass text unchanged until marker.
$parsed .= $tag . $matches[0];
$text = substr($text, strlen($matches[0]));
}
else {
# No end marker: just skip it.
$parsed .= $tag;
}
}
#
# Check for: Indented code block.
#
else if ($tag{0} == "\n" || $tag{0} == " ") {
# Indented code block: pass it unchanged, will be handled
# later.
$parsed .= $tag;
}
#
# Check for: Code span marker
# Note: need to check this after backtick fenced code blocks
#
else if ($tag{0} == "`") {
# Find corresponding end marker.
$tag_re = preg_quote($tag);
if (preg_match('{^(?>.+?|\n(?!\n))*?(?<!`)'.$tag_re.'(?!`)}',
$text, $matches))
{
# End marker found: pass text unchanged until marker.
$parsed .= $tag . $matches[0];
$text = substr($text, strlen($matches[0]));
}
else {
# Unmatched marker: just skip it.
$parsed .= $tag;
}
}
#
# Check for: Opening Block level tag or
# Opening Context Block tag (like ins and del)
# used as a block tag (tag is alone on it's line).
#
else if (preg_match('{^<(?:'.$this->block_tags_re.')\b}', $tag) ||
( preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) &&
preg_match($newline_before_re, $parsed) &&
preg_match($newline_after_re, $text) )
)
{
# Need to parse tag and following text using the HTML parser.
list($block_text, $text) =
$this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true);
# Make sure it stays outside of any paragraph by adding newlines.
$parsed .= "\n\n$block_text\n\n";
}
#
# Check for: Clean tag (like script, math)
# HTML Comments, processing instructions.
#
else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) ||
$tag{1} == '!' || $tag{1} == '?')
{
# Need to parse tag and following text using the HTML parser.
# (don't check for markdown attribute)
list($block_text, $text) =
$this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false);
$parsed .= $block_text;
}
#
# Check for: Tag with same name as enclosing tag.
#
else if ($enclosing_tag_re !== '' &&
# Same name as enclosing tag.
preg_match('{^</?(?:'.$enclosing_tag_re.')\b}', $tag))
{
#
# Increase/decrease nested tag count.
#
if ($tag{1} == '/') $depth--;
else if ($tag{strlen($tag)-2} != '/') $depth++;
if ($depth < 0) {
#
# Going out of parent element. Clean up and break so we
# return to the calling function.
#
$text = $tag . $text;
break;
}
$parsed .= $tag;
}
else {
$parsed .= $tag;
}
} while ($depth >= 0);
return array($parsed, $text);
}
protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) {
#
# Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags.
#
# * Calls $hash_method to convert any blocks.
# * Stops when the first opening tag closes.
# * $md_attr indicate if the use of the `markdown="1"` attribute is allowed.
# (it is not inside clean tags)
#
# Returns an array of that form: ( processed text , remaining text )
#
if ($text === '') return array('', '');
# Regex to match `markdown` attribute inside of a tag.
$markdown_attr_re = '
{
\s* # Eat whitespace before the `markdown` attribute
markdown
\s*=\s*
(?>
(["\']) # $1: quote delimiter
(.*?) # $2: attribute value
\1 # matching delimiter
|
([^\s>]*) # $3: unquoted attribute value
)
() # $4: make $3 always defined (avoid warnings)
}xs';
# Regex to match any tag.
$tag_re = '{
( # $2: Capture whole tag.
</? # Any opening or closing tag.
[\w:$]+ # Tag name.
(?:
(?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
(?>
".*?" | # Double quotes (can contain `>`)
\'.*?\' | # Single quotes (can contain `>`)
.+? # Anything but quotes and `>`.
)*?
)?
> # End of tag.
|
<!-- .*? --> # HTML Comment
|
<\?.*?\?> | <%.*?%> # Processing instruction
|
<!\[CDATA\[.*?\]\]> # CData Block
)
}xs';
$original_text = $text; # Save original text in case of faliure.
$depth = 0; # Current depth inside the tag tree.
$block_text = ""; # Temporary text holder for current text.
$parsed = ""; # Parsed text that will be returned.
#
# Get the name of the starting tag.
# (This pattern makes $base_tag_name_re safe without quoting.)
#
if (preg_match('/^<([\w:$]*)\b/', $text, $matches))
$base_tag_name_re = $matches[1];
#
# Loop through every tag until we find the corresponding closing tag.
#
do {
#
# Split the text using the first $tag_match pattern found.
# Text before pattern will be first in the array, text after
# pattern will be at the end, and between will be any catches made
# by the pattern.
#
$parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
if (count($parts) < 3) {
#
# End of $text reached with unbalenced tag(s).
# In that case, we return original text unchanged and pass the
# first character as filtered to prevent an infinite loop in the
# parent function.
#
return array($original_text{0}, substr($original_text, 1));
}
$block_text .= $parts[0]; # Text before current tag.
$tag = $parts[1]; # Tag to handle.
$text = $parts[2]; # Remaining text after current tag.
#
# Check for: Auto-close tag (like <hr/>)
# Comments and Processing Instructions.
#
if (preg_match('{^</?(?:'.$this->auto_close_tags_re.')\b}', $tag) ||
$tag{1} == '!' || $tag{1} == '?')
{
# Just add the tag to the block as if it was text.
$block_text .= $tag;
}
else {
#
# Increase/decrease nested tag count. Only do so if
# the tag's name match base tag's.
#
if (preg_match('{^</?'.$base_tag_name_re.'\b}', $tag)) {
if ($tag{1} == '/') $depth--;
else if ($tag{strlen($tag)-2} != '/') $depth++;
}
#
# Check for `markdown="1"` attribute and handle it.
#
if ($md_attr &&
preg_match($markdown_attr_re, $tag, $attr_m) &&
preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3]))
{
# Remove `markdown` attribute from opening tag.
$tag = preg_replace($markdown_attr_re, '', $tag);
# Check if text inside this tag must be parsed in span mode.
$this->mode = $attr_m[2] . $attr_m[3];
$span_mode = $this->mode == 'span' || $this->mode != 'block' &&
preg_match('{^<(?:'.$this->contain_span_tags_re.')\b}', $tag);
# Calculate indent before tag.
if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) {
$strlen = $this->utf8_strlen;
$indent = $strlen($matches[1], 'UTF-8');
} else {
$indent = 0;
}
# End preceding block with this tag.
$block_text .= $tag;
$parsed .= $this->$hash_method($block_text);
# Get enclosing tag name for the ParseMarkdown function.
# (This pattern makes $tag_name_re safe without quoting.)
preg_match('/^<([\w:$]*)\b/', $tag, $matches);
$tag_name_re = $matches[1];
# Parse the content using the HTML-in-Markdown parser.
list ($block_text, $text)
= $this->_hashHTMLBlocks_inMarkdown($text, $indent,
$tag_name_re, $span_mode);
# Outdent markdown text.
if ($indent > 0) {
$block_text = preg_replace("/^[ ]{1,$indent}/m", "",
$block_text);
}
# Append tag content to parsed text.
if (!$span_mode) $parsed .= "\n\n$block_text\n\n";
else $parsed .= "$block_text";
# Start over with a new block.
$block_text = "";
}
else $block_text .= $tag;
}
} while ($depth > 0);
#
# Hash last block text that wasn't processed inside the loop.
#
$parsed .= $this->$hash_method($block_text);
return array($parsed, $text);
}
protected function hashClean($text) {
#
# Called whenever a tag must be hashed when a function inserts a "clean" tag
# in $text, it passes through this function and is automaticaly escaped,
# blocking invalid nested overlap.
#
return $this->hashPart($text, 'C');
}
protected function doAnchors($text) {
#
# Turn Markdown link shortcuts into XHTML <a> tags.
#
if ($this->in_anchor) return $text;
$this->in_anchor = true;
#
# First, handle reference-style links: [link text] [id]
#
$text = preg_replace_callback('{
( # wrap whole match in $1
\[
('.$this->nested_brackets_re.') # link text = $2
\]
[ ]? # one optional space
(?:\n[ ]*)? # one optional newline followed by spaces
\[
(.*?) # id = $3
\]
)
}xs',
array($this, '_doAnchors_reference_callback'), $text);
#
# Next, inline-style links: [link text](url "optional title")
#
$text = preg_replace_callback('{
( # wrap whole match in $1
\[
('.$this->nested_brackets_re.') # link text = $2
\]
\( # literal paren
[ \n]*
(?:
<(.+?)> # href = $3
|
('.$this->nested_url_parenthesis_re.') # href = $4
)
[ \n]*
( # $5
([\'"]) # quote char = $6
(.*?) # Title = $7
\6 # matching quote
[ \n]* # ignore any spaces/tabs between closing quote and )
)? # title is optional
\)
(?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes
)
}xs',
array($this, '_doAnchors_inline_callback'), $text);
#
# Last, handle reference-style shortcuts: [link text]
# These must come last in case you've also got [link text][1]
# or [link text](/foo)
#
$text = preg_replace_callback('{
( # wrap whole match in $1
\[
([^\[\]]+) # link text = $2; can\'t contain [ or ]
\]
)
}xs',
array($this, '_doAnchors_reference_callback'), $text);
$this->in_anchor = false;
return $text;
}
protected function _doAnchors_reference_callback($matches) {
$whole_match = $matches[1];
$link_text = $matches[2];
$link_id =& $matches[3];
if ($link_id == "") {
# for shortcut links like [this][] or [this].
$link_id = $link_text;
}
# lower-case and turn embedded newlines into spaces
$link_id = strtolower($link_id);
$link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
if (isset($this->urls[$link_id])) {
$url = $this->urls[$link_id];
$url = $this->encodeURLAttribute($url);
$result = "<a href=\"$url\"";
if ( isset( $this->titles[$link_id] ) ) {
$title = $this->titles[$link_id];
$title = $this->encodeAttribute($title);
$result .= " title=\"$title\"";
}
if (isset($this->ref_attr[$link_id]))
$result .= $this->ref_attr[$link_id];
$link_text = $this->runSpanGamut($link_text);
$result .= ">$link_text</a>";
$result = $this->hashPart($result);
}
else {
$result = $whole_match;
}
return $result;
}
protected function _doAnchors_inline_callback($matches) {
$whole_match = $matches[1];
$link_text = $this->runSpanGamut($matches[2]);
$url = $matches[3] == '' ? $matches[4] : $matches[3];
$title =& $matches[7];
$attr = $this->doExtraAttributes("a", $dummy =& $matches[8]);
// if the URL was of the form <s p a c e s> it got caught by the HTML
// tag parser and hashed. Need to reverse the process before using the URL.
$unhashed = $this->unhash($url);
if ($unhashed != $url)
$url = preg_replace('/^<(.*)>$/', '\1', $unhashed);
$url = $this->encodeURLAttribute($url);
$result = "<a href=\"$url\"";
if (isset($title)) {
$title = $this->encodeAttribute($title);
$result .= " title=\"$title\"";
}
$result .= $attr;
$link_text = $this->runSpanGamut($link_text);
$result .= ">$link_text</a>";
return $this->hashPart($result);
}
protected function doImages($text) {
#
# Turn Markdown image shortcuts into <img> tags.
#
#
# First, handle reference-style labeled images: ![alt text][id]
#
$text = preg_replace_callback('{
( # wrap whole match in $1
!\[
('.$this->nested_brackets_re.') # alt text = $2
\]
[ ]? # one optional space
(?:\n[ ]*)? # one optional newline followed by spaces
\[
(.*?) # id = $3
\]
)
}xs',
array($this, '_doImages_reference_callback'), $text);
#
# Next, handle inline images: ![alt text](url "optional title")
# Don't forget: encode * and _
#
$text = preg_replace_callback('{
( # wrap whole match in $1
!\[
('.$this->nested_brackets_re.') # alt text = $2
\]
\s? # One optional whitespace character
\( # literal paren
[ \n]*
(?:
<(\S*)> # src url = $3
|
('.$this->nested_url_parenthesis_re.') # src url = $4
)
[ \n]*
( # $5
([\'"]) # quote char = $6
(.*?) # title = $7
\6 # matching quote
[ \n]*
)? # title is optional
\)
(?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes
)
}xs',
array($this, '_doImages_inline_callback'), $text);
return $text;
}
protected function _doImages_reference_callback($matches) {
$whole_match = $matches[1];
$alt_text = $matches[2];
$link_id = strtolower($matches[3]);
if ($link_id == "") {
$link_id = strtolower($alt_text); # for shortcut links like ![this][].
}
$alt_text = $this->encodeAttribute($alt_text);
if (isset($this->urls[$link_id])) {
$url = $this->encodeURLAttribute($this->urls[$link_id]);
$result = "<img src=\"$url\" alt=\"$alt_text\"";
if (isset($this->titles[$link_id])) {
$title = $this->titles[$link_id];
$title = $this->encodeAttribute($title);
$result .= " title=\"$title\"";
}
if (isset($this->ref_attr[$link_id]))
$result .= $this->ref_attr[$link_id];
$result .= $this->empty_element_suffix;
$result = $this->hashPart($result);
}
else {
# If there's no such link ID, leave intact:
$result = $whole_match;
}
return $result;
}
protected function _doImages_inline_callback($matches) {
$whole_match = $matches[1];
$alt_text = $matches[2];
$url = $matches[3] == '' ? $matches[4] : $matches[3];
$title =& $matches[7];
$attr = $this->doExtraAttributes("img", $dummy =& $matches[8]);
$alt_text = $this->encodeAttribute($alt_text);
$url = $this->encodeURLAttribute($url);
$result = "<img src=\"$url\" alt=\"$alt_text\"";
if (isset($title)) {
$title = $this->encodeAttribute($title);
$result .= " title=\"$title\""; # $title already quoted
}
$result .= $attr;
$result .= $this->empty_element_suffix;
return $this->hashPart($result);
}
protected function doHeaders($text) {
#
# Redefined to add id and class attribute support.
#
# Setext-style headers:
# Header 1 {#header1}
# ========
#
# Header 2 {#header2 .class1 .class2}
# --------
#
$text = preg_replace_callback(
'{
(^.+?) # $1: Header text
(?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes
[ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer
}mx',
array($this, '_doHeaders_callback_setext'), $text);
# atx-style headers:
# # Header 1 {#header1}
# ## Header 2 {#header2}
# ## Header 2 with closing hashes ## {#header3.class1.class2}
# ...
# ###### Header 6 {.class2}
#
$text = preg_replace_callback('{
^(\#{1,6}) # $1 = string of #\'s
[ ]*
(.+?) # $2 = Header text
[ ]*
\#* # optional closing #\'s (not counted)
(?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes
[ ]*
\n+
}xm',
array($this, '_doHeaders_callback_atx'), $text);
return $text;
}
protected function _doHeaders_callback_setext($matches) {
if ($matches[3] == '-' && preg_match('{^- }', $matches[1]))
return $matches[0];
$level = $matches[3]{0} == '=' ? 1 : 2;
$attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]);
$block = "<h$level$attr>".$this->runSpanGamut($matches[1])."</h$level>";
return "\n" . $this->hashBlock($block) . "\n\n";
}
protected function _doHeaders_callback_atx($matches) {
$level = strlen($matches[1]);
$attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]);
$block = "<h$level$attr>".$this->runSpanGamut($matches[2])."</h$level>";
return "\n" . $this->hashBlock($block) . "\n\n";
}
protected function doTables($text) {
#
# Form HTML tables.
#
$less_than_tab = $this->tab_width - 1;
#
# Find tables with leading pipe.
#
# | Header 1 | Header 2
# | -------- | --------
# | Cell 1 | Cell 2
# | Cell 3 | Cell 4
#
$text = preg_replace_callback('
{
^ # Start of a line
[ ]{0,'.$less_than_tab.'} # Allowed whitespace.
[|] # Optional leading pipe (present)
(.+) \n # $1: Header row (at least one pipe)
[ ]{0,'.$less_than_tab.'} # Allowed whitespace.
[|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline
( # $3: Cells
(?>
[ ]* # Allowed whitespace.
[|] .* \n # Row content.
)*
)
(?=\n|\Z) # Stop at final double newline.
}xm',
array($this, '_doTable_leadingPipe_callback'), $text);
#
# Find tables without leading pipe.
#
# Header 1 | Header 2
# -------- | --------
# Cell 1 | Cell 2
# Cell 3 | Cell 4
#
$text = preg_replace_callback('
{
^ # Start of a line
[ ]{0,'.$less_than_tab.'} # Allowed whitespace.
(\S.*[|].*) \n # $1: Header row (at least one pipe)
[ ]{0,'.$less_than_tab.'} # Allowed whitespace.
([-:]+[ ]*[|][-| :]*) \n # $2: Header underline
( # $3: Cells
(?>
.* [|] .* \n # Row content
)*
)
(?=\n|\Z) # Stop at final double newline.
}xm',
array($this, '_DoTable_callback'), $text);
return $text;
}
protected function _doTable_leadingPipe_callback($matches) {
$head = $matches[1];
$underline = $matches[2];
$content = $matches[3];
# Remove leading pipe for each row.
$content = preg_replace('/^ *[|]/m', '', $content);
return $this->_doTable_callback(array($matches[0], $head, $underline, $content));
}
protected function _doTable_makeAlignAttr($alignname)
{
if (empty($this->table_align_class_tmpl))
return " align=\"$alignname\"";
$classname = str_replace('%%', $alignname, $this->table_align_class_tmpl);
return " class=\"$classname\"";
}
protected function _doTable_callback($matches) {
$head = $matches[1];
$underline = $matches[2];
$content = $matches[3];
# Remove any tailing pipes for each line.
$head = preg_replace('/[|] *$/m', '', $head);
$underline = preg_replace('/[|] *$/m', '', $underline);
$content = preg_replace('/[|] *$/m', '', $content);
# Reading alignement from header underline.
$separators = preg_split('/ *[|] */', $underline);
foreach ($separators as $n => $s) {
if (preg_match('/^ *-+: *$/', $s))
$attr[$n] = $this->_doTable_makeAlignAttr('right');
else if (preg_match('/^ *:-+: *$/', $s))
$attr[$n] = $this->_doTable_makeAlignAttr('center');
else if (preg_match('/^ *:-+ *$/', $s))
$attr[$n] = $this->_doTable_makeAlignAttr('left');
else
$attr[$n] = '';
}
# Parsing span elements, including code spans, character escapes,
# and inline HTML tags, so that pipes inside those gets ignored.
$head = $this->parseSpan($head);
$headers = preg_split('/ *[|] */', $head);
$col_count = count($headers);
$attr = array_pad($attr, $col_count, '');
# Write column headers.
$text = "<table>\n";
$text .= "<thead>\n";
$text .= "<tr>\n";
foreach ($headers as $n => $header)
$text .= " <th$attr[$n]>".$this->runSpanGamut(trim($header))."</th>\n";
$text .= "</tr>\n";
$text .= "</thead>\n";
# Split content by row.
$rows = explode("\n", trim($content, "\n"));
$text .= "<tbody>\n";
foreach ($rows as $row) {
# Parsing span elements, including code spans, character escapes,
# and inline HTML tags, so that pipes inside those gets ignored.
$row = $this->parseSpan($row);
# Split row by cell.
$row_cells = preg_split('/ *[|] */', $row, $col_count);
$row_cells = array_pad($row_cells, $col_count, '');
$text .= "<tr>\n";
foreach ($row_cells as $n => $cell)
$text .= " <td$attr[$n]>".$this->runSpanGamut(trim($cell))."</td>\n";
$text .= "</tr>\n";
}
$text .= "</tbody>\n";
$text .= "</table>";
return $this->hashBlock($text) . "\n";
}
protected function doDefLists($text) {
#
# Form HTML definition lists.
#
$less_than_tab = $this->tab_width - 1;
# Re-usable pattern to match any entire dl list:
$whole_list_re = '(?>
( # $1 = whole list
( # $2
[ ]{0,'.$less_than_tab.'}
((?>.*\S.*\n)+) # $3 = defined term
\n?
[ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
)
(?s:.+?)
( # $4
\z
|
\n{2,}
(?=\S)
(?! # Negative lookahead for another term
[ ]{0,'.$less_than_tab.'}
(?: \S.*\n )+? # defined term
\n?
[ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
)
(?! # Negative lookahead for another definition
[ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
)
)
)
)'; // mx
$text = preg_replace_callback('{
(?>\A\n?|(?<=\n\n))
'.$whole_list_re.'
}mx',
array($this, '_doDefLists_callback'), $text);
return $text;
}
protected function _doDefLists_callback($matches) {
# Re-usable patterns to match list item bullets and number markers:
$list = $matches[1];
# Turn double returns into triple returns, so that we can make a
# paragraph for the last item in a list, if necessary:
$result = trim($this->processDefListItems($list));
$result = "<dl>\n" . $result . "\n</dl>";
return $this->hashBlock($result) . "\n\n";
}
protected function processDefListItems($list_str) {
#
# Process the contents of a single definition list, splitting it
# into individual term and definition list items.
#
$less_than_tab = $this->tab_width - 1;
# trim trailing blank lines:
$list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
# Process definition terms.
$list_str = preg_replace_callback('{
(?>\A\n?|\n\n+) # leading line
( # definition terms = $1
[ ]{0,'.$less_than_tab.'} # leading whitespace
(?!\:[ ]|[ ]) # negative lookahead for a definition
# mark (colon) or more whitespace.
(?> \S.* \n)+? # actual term (not whitespace).
)
(?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed
# with a definition mark.
}xm',
array($this, '_processDefListItems_callback_dt'), $list_str);
# Process actual definitions.
$list_str = preg_replace_callback('{
\n(\n+)? # leading line = $1
( # marker space = $2
[ ]{0,'.$less_than_tab.'} # whitespace before colon
\:[ ]+ # definition mark (colon)
)
((?s:.+?)) # definition text = $3
(?= \n+ # stop at next definition mark,
(?: # next term or end of text
[ ]{0,'.$less_than_tab.'} \:[ ] |
<dt> | \z
)
)
}xm',
array($this, '_processDefListItems_callback_dd'), $list_str);
return $list_str;
}
protected function _processDefListItems_callback_dt($matches) {
$terms = explode("\n", trim($matches[1]));
$text = '';
foreach ($terms as $term) {
$term = $this->runSpanGamut(trim($term));
$text .= "\n<dt>" . $term . "</dt>";
}
return $text . "\n";
}
protected function _processDefListItems_callback_dd($matches) {
$leading_line = $matches[1];
$marker_space = $matches[2];
$def = $matches[3];
if ($leading_line || preg_match('/\n{2,}/', $def)) {
# Replace marker with the appropriate whitespace indentation
$def = str_repeat(' ', strlen($marker_space)) . $def;
$def = $this->runBlockGamut($this->outdent($def . "\n\n"));
$def = "\n". $def ."\n";
}
else {
$def = rtrim($def);
$def = $this->runSpanGamut($this->outdent($def));
}
return "\n<dd>" . $def . "</dd>\n";
}
protected function doFencedCodeBlocks($text) {
#
# Adding the fenced code block syntax to regular Markdown:
#
# ~~~
# Code block
# ~~~
#
$less_than_tab = $this->tab_width;
$text = preg_replace_callback('{
(?:\n|\A)
# 1: Opening marker
(
(?:~{3,}|`{3,}) # 3 or more tildes/backticks.
)
[ ]*
(?:
\.?([-_:a-zA-Z0-9]+) # 2: standalone class name
|
'.$this->id_class_attr_catch_re.' # 3: Extra attributes
)?
[ ]* \n # Whitespace and newline following marker.
# 4: Content
(
(?>
(?!\1 [ ]* \n) # Not a closing marker.
.*\n+
)+
)
# Closing marker.
\1 [ ]* (?= \n )
}xm',
array($this, '_doFencedCodeBlocks_callback'), $text);
return $text;
}
protected function _doFencedCodeBlocks_callback($matches) {
$classname =& $matches[2];
$attrs =& $matches[3];
$codeblock = $matches[4];
$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
$codeblock = preg_replace_callback('/^\n+/',
array($this, '_doFencedCodeBlocks_newlines'), $codeblock);
if ($classname != "") {
if ($classname{0} == '.')
$classname = substr($classname, 1);
$attr_str = ' class="'.$this->code_class_prefix.$classname.'"';
} else {
$attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs);
}
$pre_attr_str = $this->code_attr_on_pre ? $attr_str : '';
$code_attr_str = $this->code_attr_on_pre ? '' : $attr_str;
$codeblock = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>";
return "\n\n".$this->hashBlock($codeblock)."\n\n";
}
protected function _doFencedCodeBlocks_newlines($matches) {
return str_repeat("<br$this->empty_element_suffix",
strlen($matches[0]));
}
#
# Redefining emphasis markers so that emphasis by underscore does not
# work in the middle of a word.
#
protected $em_relist = array(
'' => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?![\.,:;]?\s)',
'*' => '(?<![\s*])\*(?!\*)',
'_' => '(?<![\s_])_(?![a-zA-Z0-9_])',
);
protected $strong_relist = array(
'' => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?![\.,:;]?\s)',
'**' => '(?<![\s*])\*\*(?!\*)',
'__' => '(?<![\s_])__(?![a-zA-Z0-9_])',
);
protected $em_strong_relist = array(
'' => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?![\.,:;]?\s)',
'***' => '(?<![\s*])\*\*\*(?!\*)',
'___' => '(?<![\s_])___(?![a-zA-Z0-9_])',
);
protected function formParagraphs($text) {
#
# Params:
# $text - string to process with html <p> tags
#
# Strip leading and trailing lines:
$text = preg_replace('/\A\n+|\n+\z/', '', $text);
$grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
#
# Wrap <p> tags and unhashify HTML blocks
#
foreach ($grafs as $key => $value) {
$value = trim($this->runSpanGamut($value));
# Check if this should be enclosed in a paragraph.
# Clean tag hashes & block tag hashes are left alone.
$is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value);
if ($is_p) {
$value = "<p>$value</p>";
}
$grafs[$key] = $value;
}
# Join grafs in one text, then unhash HTML tags.
$text = implode("\n\n", $grafs);
# Finish by removing any tag hashes still present in $text.
$text = $this->unhash($text);
return $text;
}
### Footnotes
protected function stripFootnotes($text) {
#
# Strips link definitions from text, stores the URLs and titles in
# hash references.
#
$less_than_tab = $this->tab_width - 1;
# Link defs are in the form: [^id]: url "optional title"
$text = preg_replace_callback('{
^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1
[ ]*
\n? # maybe *one* newline
( # text = $2 (no blank lines allowed)
(?:
.+ # actual text
|
\n # newlines but
(?!\[.+?\][ ]?:\s)# negative lookahead for footnote or link definition marker.
(?!\n+[ ]{0,3}\S)# ensure line is not blank and followed
# by non-indented content
)*
)
}xm',
array($this, '_stripFootnotes_callback'),
$text);
return $text;
}
protected function _stripFootnotes_callback($matches) {
$note_id = $this->fn_id_prefix . $matches[1];
$this->footnotes[$note_id] = $this->outdent($matches[2]);
return ''; # String that will replace the block
}
protected function doFootnotes($text) {
#
# Replace footnote references in $text [^id] with a special text-token
# which will be replaced by the actual footnote marker in appendFootnotes.
#
if (!$this->in_anchor) {
$text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text);
}
return $text;
}
protected function appendFootnotes($text) {
#
# Append footnote list to text.
#
$text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
array($this, '_appendFootnotes_callback'), $text);
if (!empty($this->footnotes_ordered)) {
$text .= "\n\n";
$text .= "<div class=\"footnotes\">\n";
$text .= "<hr". $this->empty_element_suffix ."\n";
$text .= "<ol>\n\n";
$attr = "";
if ($this->fn_backlink_class != "") {
$class = $this->fn_backlink_class;
$class = $this->encodeAttribute($class);
$attr .= " class=\"$class\"";
}
if ($this->fn_backlink_title != "") {
$title = $this->fn_backlink_title;
$title = $this->encodeAttribute($title);
$attr .= " title=\"$title\"";
}
$num = 0;
while (!empty($this->footnotes_ordered)) {
$footnote = reset($this->footnotes_ordered);
$note_id = key($this->footnotes_ordered);
unset($this->footnotes_ordered[$note_id]);
$ref_count = $this->footnotes_ref_count[$note_id];
unset($this->footnotes_ref_count[$note_id]);
unset($this->footnotes[$note_id]);
$footnote .= "\n"; # Need to append newline before parsing.
$footnote = $this->runBlockGamut("$footnote\n");
$footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
array($this, '_appendFootnotes_callback'), $footnote);
$attr = str_replace("%%", ++$num, $attr);
$note_id = $this->encodeAttribute($note_id);
# Prepare backlink, multiple backlinks if multiple references
$backlink = "<a href=\"#fnref:$note_id\"$attr>&#8617;</a>";
for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) {
$backlink .= " <a href=\"#fnref$ref_num:$note_id\"$attr>&#8617;</a>";
}
# Add backlink to last paragraph; create new paragraph if needed.
if (preg_match('{</p>$}', $footnote)) {
$footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>";
} else {
$footnote .= "\n\n<p>$backlink</p>";
}
$text .= "<li id=\"fn:$note_id\">\n";
$text .= $footnote . "\n";
$text .= "</li>\n\n";
}
$text .= "</ol>\n";
$text .= "</div>";
}
return $text;
}
protected function _appendFootnotes_callback($matches) {
$node_id = $this->fn_id_prefix . $matches[1];
# Create footnote marker only if it has a corresponding footnote *and*
# the footnote hasn't been used by another marker.
if (isset($this->footnotes[$node_id])) {
$num =& $this->footnotes_numbers[$node_id];
if (!isset($num)) {
# Transfer footnote content to the ordered list and give it its
# number
$this->footnotes_ordered[$node_id] = $this->footnotes[$node_id];
$this->footnotes_ref_count[$node_id] = 1;
$num = $this->footnote_counter++;
$ref_count_mark = '';
} else {
$ref_count_mark = $this->footnotes_ref_count[$node_id] += 1;
}
$attr = "";
if ($this->fn_link_class != "") {
$class = $this->fn_link_class;
$class = $this->encodeAttribute($class);
$attr .= " class=\"$class\"";
}
if ($this->fn_link_title != "") {
$title = $this->fn_link_title;
$title = $this->encodeAttribute($title);
$attr .= " title=\"$title\"";
}
$attr = str_replace("%%", $num, $attr);
$node_id = $this->encodeAttribute($node_id);
return
"<sup id=\"fnref$ref_count_mark:$node_id\">".
"<a href=\"#fn:$node_id\"$attr>$num</a>".
"</sup>";
}
return "[^".$matches[1]."]";
}
### Abbreviations ###
protected function stripAbbreviations($text) {
#
# Strips abbreviations from text, stores titles in hash references.
#
$less_than_tab = $this->tab_width - 1;
# Link defs are in the form: [id]*: url "optional title"
$text = preg_replace_callback('{
^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1
(.*) # text = $2 (no blank lines allowed)
}xm',
array($this, '_stripAbbreviations_callback'),
$text);
return $text;
}
protected function _stripAbbreviations_callback($matches) {
$abbr_word = $matches[1];
$abbr_desc = $matches[2];
if ($this->abbr_word_re)
$this->abbr_word_re .= '|';
$this->abbr_word_re .= preg_quote($abbr_word);
$this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
return ''; # String that will replace the block
}
protected function doAbbreviations($text) {
#
# Find defined abbreviations in text and wrap them in <abbr> elements.
#
if ($this->abbr_word_re) {
// cannot use the /x modifier because abbr_word_re may
// contain significant spaces:
$text = preg_replace_callback('{'.
'(?<![\w\x1A])'.
'(?:'.$this->abbr_word_re.')'.
'(?![\w\x1A])'.
'}',
array($this, '_doAbbreviations_callback'), $text);
}
return $text;
}
protected function _doAbbreviations_callback($matches) {
$abbr = $matches[0];
if (isset($this->abbr_desciptions[$abbr])) {
$desc = $this->abbr_desciptions[$abbr];
if (empty($desc)) {
return $this->hashPart("<abbr>$abbr</abbr>");
} else {
$desc = $this->encodeAttribute($desc);
return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>");
}
} else {
return $matches[0];
}
}
}

View file

@ -1,10 +1,10 @@
<?php
# Use this file if you cannot use class autoloading. It will include all the
# files needed for the MarkdownExtra parser.
#
# Take a look at the PSR-0-compatible class autoloading implementation
# in the Readme.php file if you want a simple autoloader setup.
// Use this file if you cannot use class autoloading. It will include all the
// files needed for the MarkdownExtra parser.
//
// Take a look at the PSR-0-compatible class autoloading implementation
// in the Readme.php file if you want a simple autoloader setup.
require_once dirname(__FILE__) . '/MarkdownInterface.php';
require_once dirname(__FILE__) . '/Markdown.php';

View file

@ -1,38 +1,1785 @@
<?php
#
# Markdown Extra - A text-to-HTML conversion tool for web writers
#
# PHP Markdown Extra
# Copyright (c) 2004-2014 Michel Fortin
# <http://michelf.com/projects/php-markdown/>
#
# Original Markdown
# Copyright (c) 2004-2006 John Gruber
# <http://daringfireball.net/projects/markdown/>
#
/**
* Markdown Extra - A text-to-HTML conversion tool for web writers
*
* @package php-markdown
* @author Michel Fortin <michel.fortin@michelf.com>
* @copyright 2004-2016 Michel Fortin <https://michelf.com/projects/php-markdown/>
* @copyright (Original Markdown) 2004-2006 John Gruber <https://daringfireball.net/projects/markdown/>
*/
namespace Michelf;
/**
* Markdown Extra Parser Class
*/
class MarkdownExtra extends \Michelf\Markdown {
/**
* Configuration variables
*/
# Just force Michelf/Markdown.php to load. This is needed to load
# the temporary implementation class. See below for details.
\Michelf\Markdown::MARKDOWNLIB_VERSION;
/**
* Prefix for footnote ids.
* @var string
*/
public $fn_id_prefix = "";
/**
* Optional title attribute for footnote links and backlinks.
* @var string
*/
public $fn_link_title = "";
public $fn_backlink_title = "";
/**
* Optional class attribute for footnote links and backlinks.
* @var string
*/
public $fn_link_class = "footnote-ref";
public $fn_backlink_class = "footnote-backref";
#
# Markdown Extra Parser Class
#
# Note: Currently the implementation resides in the temporary class
# \Michelf\MarkdownExtra_TmpImpl (in the same file as \Michelf\Markdown).
# This makes it easier to propagate the changes between the three different
# packaging styles of PHP Markdown. Once this issue is resolved, the
# _MarkdownExtra_TmpImpl will disappear and this one will contain the code.
#
/**
* Content to be displayed within footnote backlinks. The default is '↩';
* the U+FE0E on the end is a Unicode variant selector used to prevent iOS
* from displaying the arrow character as an emoji.
* @var string
*/
public $fn_backlink_html = '&#8617;&#xFE0E;';
class MarkdownExtra extends \Michelf\_MarkdownExtra_TmpImpl {
/**
* Class name for table cell alignment (%% replaced left/center/right)
* For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center'
* If empty, the align attribute is used instead of a class name.
* @var string
*/
public $table_align_class_tmpl = '';
### Parser Implementation ###
/**
* Optional class prefix for fenced code block.
* @var string
*/
public $code_class_prefix = "";
# Temporarily, the implemenation is in the _MarkdownExtra_TmpImpl class.
# See note above.
/**
* Class attribute for code blocks goes on the `code` tag;
* setting this to true will put attributes on the `pre` tag instead.
* @var boolean
*/
public $code_attr_on_pre = false;
/**
* Predefined abbreviations.
* @var array
*/
public $predef_abbr = array();
/**
* Parser implementation
*/
/**
* Constructor function. Initialize the parser object.
* @return void
*/
public function __construct() {
// Add extra escapable characters before parent constructor
// initialize the table.
$this->escape_chars .= ':|';
// Insert extra document, block, and span transformations.
// Parent constructor will do the sorting.
$this->document_gamut += array(
"doFencedCodeBlocks" => 5,
"stripFootnotes" => 15,
"stripAbbreviations" => 25,
"appendFootnotes" => 50,
);
$this->block_gamut += array(
"doFencedCodeBlocks" => 5,
"doTables" => 15,
"doDefLists" => 45,
);
$this->span_gamut += array(
"doFootnotes" => 5,
"doAbbreviations" => 70,
);
$this->enhanced_ordered_list = true;
parent::__construct();
}
/**
* Extra variables used during extra transformations.
* @var array
*/
protected $footnotes = array();
protected $footnotes_ordered = array();
protected $footnotes_ref_count = array();
protected $footnotes_numbers = array();
protected $abbr_desciptions = array();
/** @var string */
protected $abbr_word_re = '';
/**
* Give the current footnote number.
* @var integer
*/
protected $footnote_counter = 1;
/**
* Setting up Extra-specific variables.
*/
protected function setup() {
parent::setup();
$this->footnotes = array();
$this->footnotes_ordered = array();
$this->footnotes_ref_count = array();
$this->footnotes_numbers = array();
$this->abbr_desciptions = array();
$this->abbr_word_re = '';
$this->footnote_counter = 1;
foreach ($this->predef_abbr as $abbr_word => $abbr_desc) {
if ($this->abbr_word_re)
$this->abbr_word_re .= '|';
$this->abbr_word_re .= preg_quote($abbr_word);
$this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
}
}
/**
* Clearing Extra-specific variables.
*/
protected function teardown() {
$this->footnotes = array();
$this->footnotes_ordered = array();
$this->footnotes_ref_count = array();
$this->footnotes_numbers = array();
$this->abbr_desciptions = array();
$this->abbr_word_re = '';
parent::teardown();
}
/**
* Extra attribute parser
*/
/**
* Expression to use to catch attributes (includes the braces)
* @var string
*/
protected $id_class_attr_catch_re = '\{((?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,})[ ]*\}';
/**
* Expression to use when parsing in a context when no capture is desired
* @var string
*/
protected $id_class_attr_nocatch_re = '\{(?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,}[ ]*\}';
/**
* Parse attributes caught by the $this->id_class_attr_catch_re expression
* and return the HTML-formatted list of attributes.
*
* Currently supported attributes are .class and #id.
*
* In addition, this method also supports supplying a default Id value,
* which will be used to populate the id attribute in case it was not
* overridden.
* @param string $tag_name
* @param string $attr
* @param mixed $defaultIdValue
* @param array $classes
* @return string
*/
protected function doExtraAttributes($tag_name, $attr, $defaultIdValue = null, $classes = array()) {
if (empty($attr) && !$defaultIdValue && empty($classes)) return "";
// Split on components
preg_match_all('/[#.a-z][-_:a-zA-Z0-9=]+/', $attr, $matches);
$elements = $matches[0];
// Handle classes and IDs (only first ID taken into account)
$attributes = array();
$id = false;
foreach ($elements as $element) {
if ($element{0} == '.') {
$classes[] = substr($element, 1);
} else if ($element{0} == '#') {
if ($id === false) $id = substr($element, 1);
} else if (strpos($element, '=') > 0) {
$parts = explode('=', $element, 2);
$attributes[] = $parts[0] . '="' . $parts[1] . '"';
}
}
if (!$id) $id = $defaultIdValue;
// Compose attributes as string
$attr_str = "";
if (!empty($id)) {
$attr_str .= ' id="'.$this->encodeAttribute($id) .'"';
}
if (!empty($classes)) {
$attr_str .= ' class="'. implode(" ", $classes) . '"';
}
if (!$this->no_markup && !empty($attributes)) {
$attr_str .= ' '.implode(" ", $attributes);
}
return $attr_str;
}
/**
* Strips link definitions from text, stores the URLs and titles in
* hash references.
* @param string $text
* @return string
*/
protected function stripLinkDefinitions($text) {
$less_than_tab = $this->tab_width - 1;
// Link defs are in the form: ^[id]: url "optional title"
$text = preg_replace_callback('{
^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1
[ ]*
\n? # maybe *one* newline
[ ]*
(?:
<(.+?)> # url = $2
|
(\S+?) # url = $3
)
[ ]*
\n? # maybe one newline
[ ]*
(?:
(?<=\s) # lookbehind for whitespace
["(]
(.*?) # title = $4
[")]
[ ]*
)? # title is optional
(?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr
(?:\n+|\Z)
}xm',
array($this, '_stripLinkDefinitions_callback'),
$text);
return $text;
}
/**
* Strip link definition callback
* @param array $matches
* @return string
*/
protected function _stripLinkDefinitions_callback($matches) {
$link_id = strtolower($matches[1]);
$url = $matches[2] == '' ? $matches[3] : $matches[2];
$this->urls[$link_id] = $url;
$this->titles[$link_id] =& $matches[4];
$this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]);
return ''; // String that will replace the block
}
/**
* HTML block parser
*/
/**
* Tags that are always treated as block tags
* @var string
*/
protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|figure';
/**
* Tags treated as block tags only if the opening tag is alone on its line
* @var string
*/
protected $context_block_tags_re = 'script|noscript|style|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video';
/**
* Tags where markdown="1" default to span mode:
* @var string
*/
protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address';
/**
* Tags which must not have their contents modified, no matter where
* they appear
* @var string
*/
protected $clean_tags_re = 'script|style|math|svg';
/**
* Tags that do not need to be closed.
* @var string
*/
protected $auto_close_tags_re = 'hr|img|param|source|track';
/**
* Hashify HTML Blocks and "clean tags".
*
* We only want to do this for block-level HTML tags, such as headers,
* lists, and tables. That's because we still want to wrap <p>s around
* "paragraphs" that are wrapped in non-block-level tags, such as anchors,
* phrase emphasis, and spans. The list of tags we're looking for is
* hard-coded.
*
* This works by calling _HashHTMLBlocks_InMarkdown, which then calls
* _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1"
* attribute is found within a tag, _HashHTMLBlocks_InHTML calls back
* _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
* These two functions are calling each other. It's recursive!
* @param string $text
* @return string
*/
protected function hashHTMLBlocks($text) {
if ($this->no_markup) {
return $text;
}
// Call the HTML-in-Markdown hasher.
list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text);
return $text;
}
/**
* Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags.
*
* * $indent is the number of space to be ignored when checking for code
* blocks. This is important because if we don't take the indent into
* account, something like this (which looks right) won't work as expected:
*
* <div>
* <div markdown="1">
* Hello World. <-- Is this a Markdown code block or text?
* </div> <-- Is this a Markdown code block or a real tag?
* <div>
*
* If you don't like this, just don't indent the tag on which
* you apply the markdown="1" attribute.
*
* * If $enclosing_tag_re is not empty, stops at the first unmatched closing
* tag with that name. Nested tags supported.
*
* * If $span is true, text inside must treated as span. So any double
* newline will be replaced by a single newline so that it does not create
* paragraphs.
*
* Returns an array of that form: ( processed text , remaining text )
*
* @param string $text
* @param integer $indent
* @param string $enclosing_tag_re
* @param boolean $span
* @return array
*/
protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0,
$enclosing_tag_re = '', $span = false)
{
if ($text === '') return array('', '');
// Regex to check for the presense of newlines around a block tag.
$newline_before_re = '/(?:^\n?|\n\n)*$/';
$newline_after_re =
'{
^ # Start of text following the tag.
(?>[ ]*<!--.*?-->)? # Optional comment.
[ ]*\n # Must be followed by newline.
}xs';
// Regex to match any tag.
$block_tag_re =
'{
( # $2: Capture whole tag.
</? # Any opening or closing tag.
(?> # Tag name.
' . $this->block_tags_re . ' |
' . $this->context_block_tags_re . ' |
' . $this->clean_tags_re . ' |
(?!\s)'.$enclosing_tag_re . '
)
(?:
(?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
(?>
".*?" | # Double quotes (can contain `>`)
\'.*?\' | # Single quotes (can contain `>`)
.+? # Anything but quotes and `>`.
)*?
)?
> # End of tag.
|
<!-- .*? --> # HTML Comment
|
<\?.*?\?> | <%.*?%> # Processing instruction
|
<!\[CDATA\[.*?\]\]> # CData Block