<?php
/* Given a string containing only a template expression surrounded by curly brackets,
 * returns a structure that can be modified and then converted back into a string
 * Status: unstable, parameters and return value may change */
function wp_parse_template($text) {
    if (!ereg("^{{(.*)}}$", $text, $regs))
	trigger_error("Incomplete template", E_USER_ERROR);
    $text = $regs[1];
    if (strpos($text, "{") !== false)
	trigger_error("No support for complex templates", E_USER_ERROR);
    $args = explode("|", $text);
    $ret = array();
    $ret["title"] = array_shift($args);
    $rargs = array();
    foreach ($args as $a) {
	$pair = explode("=", $a);
	if (count($pair) == 2) {
	    $rargs[trim($pair[0])] = $pair[1];
	    if (trim($pair[0]) != $pair[0])
		$pmap[trim($pair[0])] = $pair[0];
	}
	else if (count($pair) == 1)
	    $rargs[] = $pair[0];
	else
	    trigger_error("Unhandled template argument \"".$a."\a", E_USER_WARNING);
    }
    $ret["args"] = $rargs;
    if (!empty($pmap))
	$ret["pmap"] = $pmap;
    return $ret;
}

/* Returns an empty but titled template structure
 * Status: unstable, parameters may change */
function wptmpl_create($title) {
    return array("title" => $title, "args" => array());
}

/* Sets a named template parameter
 * Status: unstable, parameters may change */
function wptmpl_set_arg(&$tmpl, $param, $val) {
    $tmpl["args"][$param] = $val;
}

/* Removes a named template parameter
 * Status: unstable, parameters may change */
function wptmpl_unset_arg(&$tmpl, $param) {
    $tmpl["args"][$param] = null;
}

/* Gets the value a named template parameter
 * Status: unstable, parameters may change */
function wptmpl_get_arg($tmpl, $param) {
    if (!array_key_exists($param, $tmpl["args"]))
	return null;
    return $tmpl["args"][$param];
}

/* Returns true if the template has a parameter with the given name
 * Status: unstable, parameters may change */
function wptmpl_has_arg($tmpl, $param) {
    if (!array_key_exists($param, $tmpl["args"]))
	return false;
    return isset($tmpl["args"][$param]);
}

/* Changes the template name
 * Status: unstable, parameters may change */
function wptmpl_set_title(&$tmpl, $title) {
    $tmpl["title"] = $title;
}

/* Changes the template name
 * Status: unstable, parameters may change */
function wptmpl_get_title($tmpl) {
    return $tmpl["title"];
}

/* Returns a string from the template structure
 * Status: unstable, parameters may change */
function wp_build_template($tmpl) {
    $text = "{{".$tmpl["title"];
    if (!array_key_exists("args", $tmpl))
	return $text."}}";
    $args = $tmpl["args"];
    if (array_key_exists("pmap", $tmpl))
	$pmap = $tmpl["pmap"];
    foreach ($args as $k => $v) {
	if (!isset($v))
	    continue;
	else if (is_string($k) && isset($pmap) && array_key_exists($k, $pmap))
	    $text .= "|".$pmap[$k]."=".$v;
	else if (is_string($k))
	    $text .= "|".$k."=".$v;
	else
	    $text .= "|".$v;
    }
    return $text."}}";
}

/* Finds the first full template with the given name in the text
 * Status: unstable, parameters may change */
function wp_find_template($title, $text, $ignore_case = false) {
    $regchars = ".[]{}*?";
    if (!eregi("({{[[:space:]]*(".addcslashes($title, $regchars).")[[:space:]]*[|}]+(.*))$", $text, $regs))
	return null;
    $tstr = $regs[1];
    if (!$ignore_case && ucfirst($regs[2]) != ucfirst($title))
	return wp_find_template($regs[3]);
    $l = 0;
    $len = strlen($tstr);
    for ($i = 0; $i < $len; $i++) {
	if ($tstr[$i] == "{")
	    $l++;
	else if ($tstr[$i] == "}")
	    $l--;
	if ($l <= 0)
	    break;
    }
    if ($l > 0)
	return null;
    return substr($tstr, 0, $i + 1);
}

/* Bot exclusion detector, returns false if the text contains a directive disallowing
 * this bot
 * Status: unstable, parameters may change */
function wp_page_allows_bot($text, $context = null, $messages = null) {
    $tstr = wp_find_template("Nobots", $text, true);
    if (!empty($tstr))
	return false;
    $tstr = wp_find_template("Bots", $text, true);
    if (empty($tstr))
	return true;
    $tmpl = wp_parse_template($tstr);
    if (isset($context) && array_key_exists("username", $context))
	$botname = $context["username"];
    if (array_key_exists("deny", $tmpl["args"])) {
	$denied = explode(",", $tmpl["args"]["deny"]);
	foreach ($denied as $d) {
	    $d = trim($d);
	    if (strtolower($d) == "all")
		return false;
	    if (isset($botname) && $d == $botname)
		return false;
	}
    }
    if (array_key_exists("allow", $tmpl["args"])) {
	$allowed = explode(",", $tmpl["args"]["allow"]);
	foreach ($allowed as $a) {
	    $a = trim($a);
	    if (strtolower($a) == "none")
		return false;
	}
    }
    if (array_key_exists("optout", $tmpl["args"]) && isset($messages)) {
	$optout = explode(",", $tmpl["args"]["optout"]);
	foreach ($optout as $o) {
	    $o = trim($o);
	    if (strtolower($o) == "all")
		return false;
	    if (is_string($messages) && strtolower($o) == strtolower($messages))
		return false;
	    if (is_array($messages) && in_array($o, $messages))
		return false;
	}
    }
    return true;
}

/* Returns an integer timestamp for the date the comment was signed, or null if no signature
 * was found. */
function wp_date_comment($text) {
    if (!eregi("\[\[user.* ([0-9]+:[0-9]+, [0-9]+ [a-z]+ [0-9]+ \(utc\))", $text, $regs))
	return null;
    $time = strtotime($regs[1]);
    if ($time > time())
	return null;
    return $time;
}

/* Returns a date string formatted for POSTing, for the given UNIX timestamp. */
function wp_format_post_date($ts) {
    return gmdate("Y-m-d\TH:i:s\Z", $ts);
}

/* Returns an array of the transcluded subpages. The subpages are the keys, the section
 * names are the values. */
function wp_list_subpages($title, $page) {
    $subpages = array();
    $lines = explode("\n", $page);
    foreach ($lines as $line) {
	$line = trim($line);
	if (ereg("==(.*)==", $line, $regs)) {
	    $section = trim($regs[1]);
	    continue;
	}
	$line = str_replace("_", " ", $line);
	$regchars = ".[]{}*?";
	if (ereg("{{".addcslashes($title, $regchars)."/(.*)}}", $line, $regs))
	    $subpages[rtrim($regs[1])] = $section;
    }
    return $subpages;
}


/* POSTs the array of data to the wiki
 * Status: stable */
function wp_post($post, $context = null) {
    $url = "http://en.wikipedia.org/w/api.php";
    if (isset($context) && array_key_exists("api url", $context))
	$url = $context["api url"];
    $header = "Content-type: application/x-www-form-urlencoded\n";
    if (isset($context) && array_key_exists("cookies", $context))
	$header .= "Cookie: ".http_build_query($context["cookies"], "", "; ")."\n";
    $http_opts = array(
	"http" => array(
	    "method" => "POST",
	    "header" => $header,
	    "content" => http_build_query($post)
	)
    );
    $sctx = stream_context_create($http_opts);
    while (true) {
	$ret = file_get_contents($url, 0, $sctx);
	if (strstr($ret, "maxlag")) {
	    sleep(5);
	    continue;
	}
	break;
    }
    return $ret;
}

/* Downloads the page contents from the wiki
 * Status: stable */
function wp_get($title, $context = null, &$timestamp = null) {
    $titles = array($title);
    $timestamps = array();
    $pages = wp_get_multiple($titles, $context, $timestamps);
    $timestamp = $timestamps[$title];
    return $pages[$title];
}

/* Returns an associative array of the contents of all the specified pages
 * Status: stable */
function wp_get_multiple($titles, $context = null, &$timestamps = null) {
    $timestamps = array();
    $pages = array();
    if (empty($titles))
	return $pages;
    $post = array(
	"action" => "query",
	"format" => "php",
	"prop" => "revisions",
	"titles" => implode("|", $titles),
	"rvprop" => "timestamp|content",
    );
    if (isset($context) && array_key_exists("maxlag", $context))
	$post["maxlag"] = (string)$context["maxlag"];
    $ret = wp_post($post, $context);
    $ret = unserialize($ret);
    $pinfo = $ret["query"]["pages"];
    if (!isset($pinfo))
	return array();
    $revs = array();
    foreach ($pinfo as $p) {
	$t = $p["title"];
	if (empty($p["revisions"]))
	    continue;
	$a = array_shift($p["revisions"]);
	$timestamps[$t] = strtotime($a["timestamp"]);
	$pages[$t] = $a["*"];
    }
    return $pages;
}

/* Creates a context structure to be passed to the other functions, also sets
 * various options
 * Status: unstable, parameters and return value may change */
function wp_create_context($maxlag = null, $bot = false, $api_url = null) {
    $context = array();
    if (isset($maxlag))
	$context["maxlag"] = $maxlag;
    if (isset($bot))
	$context["bot"] = $bot;
    if (isset($api_url))
	$context["api url"] = $api_url;
    return $context;
}

/* Sets the number of items to be returned for each query
 * Status: stable */
function wp_context_set_query_limit($limit, &$context) {
    $context["qlimit"] = $limit;
}

/* Logs the bot into the wiki associated with the given context
 * Status: unstable, parameters and return value may change */
function wp_login($username, $password, &$context) {
    if (!isset($username) || $username == "")
	trigger_error("Username not set", E_USER_ERROR);
    if (!isset($password))
	trigger_error("Password not set", E_USER_ERROR);
    $login_post = array(
	"action" => "login",
	"format" => "php",
	"lgname" => $username,
	"lgpassword" => $password,
    );
    $ret = wp_post($login_post, $context);
    $ret = unserialize($ret);
    if (!array_key_exists("login", $ret))
	return false;
    $login = $ret["login"];
    if ($login["result"] != "Success")
	return false;
    $prefix = $login["cookieprefix"];
    $cookies = array($prefix."UserName" => $settings["username"]);
    if (array_key_exists("lguserid", $login))
	$cookies[$prefix."UserID"] = $login["lguserid"];
    if (array_key_exists("lgtoken", $login))
	$cookies[$prefix."Token"] = $login["lgtoken"];
    if (array_key_exists("sessionid", $login))
	$cookies[$prefix."_session"] = $login["sessionid"];
    if (!isset($context))
	$context = array();
    $context["username"] = $username;
    $context["cookies"] = $cookies;
    return true;
}

/* Logs out of the wiki
 * Status: stable */
function wp_logout($context) {
    $post = array(
	"action" => "logout",
	"format" => "php",
    );
    wp_post($post, $context);
}

/* Returns an edit token to be used for all edits in the session
 * Status: stable */
function wp_get_edit_token($title, $context) {
    if (!isset($context) || !isset($context["cookies"]))
	trigger_error("Must be logged in to get edit token", E_USER_ERROR);
    $post = array(
	"action" => "query",
	"format" => "php",
	"prop" => "info",
	"intoken" => "edit",
	"titles" => $title,
    );
    if (isset($context) && array_key_exists("maxlag", $context))
	$post["maxlag"] = (string)$context["maxlag"];
    $ret = wp_post($post, $context);
    $ret = unserialize($ret);
    $pages = $ret["query"]["pages"];
    foreach ($pages as $p) {
	if ($p["title"] == $title)
	    return $p["edittoken"];
    }
    return "";
}

/* Uploads a new page or section over the existing one
 * Status: stable */
function wp_edit_section($title, $content, $summary, $section, $edtoken, $context,
	$timestamp = null) {
    if (!isset($context) || !isset($context["cookies"]))
	trigger_error("Must be logged in to edit pages", E_USER_ERROR);
    if (!wp_page_allows_bot($content, $context))
	trigger_error($title." excludes bot edits", E_USER_ERROR);
    $post = array(
	"action" => "edit",
	"format" => "php",
	"title" => $title,
	"text" => $content,
	"token" => $edtoken,
	"summary" => $summary,
    );
    if (array_key_exists("maxlag", $context))
	$post["maxlag"] = (string)$context["maxlag"];
    if (array_key_exists("bot", $context) && $context["bot"])
	$post["bot"] = "yes";
    if (isset($timestamp))
	$post["basetimestamp"] = wp_format_post_date($timestamp);
    if (isset($section))
	$post["section"] = $section;
    $ret = wp_post($post, $context);
    $ret = unserialize($ret);
    if (isset($ret["error"])) {
	trigger_error($ret["error"]["code"], E_USER_NOTICE);
	return false;
    }
    if (isset($ret["edit"]) && $ret["edit"]["result"] == "Success")
	return true;
    trigger_error("Unhandled query return status", E_USER_WARNING);
    return false;
}

/* Uploads a new page over the existing one
 * Status: stable */
function wp_edit_page($title, $content, $summary, $edtoken, $context,
	$timestamp = null) {
    return wp_edit_section($title, $content, $summary, null, $edtoken, $context, $timestamp);
}

/* Posts a new section to the page
 * Status: stable */
function wp_append_section($ptitle, $stitle, $content, $edtoken, $context) {
    return wp_edit_section($ptitle, $content, $stitle, "new", $edtoken, $context);
}

/* Keeps trying to modify the page until it is successful. The modifications are made by
 * the passed in function, with these parameters:
 * $new_page = $modify($old_page, $data); */
function wp_edit_war($title, $summary, $modify, $data, $ctx, $token = null,
	$old_page = null, $old_ts = null, $max_tries = null) {
    if (!isset($token))
	$token = wp_get_edit_token($title, $ctx);
    $tries = 0;
    while (true) {
	unset($last_ts);
	if (isset($old_page) && isset($old_ts)) {
	    $old = $old_page;
	    $last_ts = $old_ts;
	    unset($old_page);
	    unset($old_ts);
	}
	else
	    $old = wp_get($title, $ctx, $last_ts);
	if (!wp_page_allows_bot($old, $ctx))
	    trigger_error($title." excludes bot edits", E_USER_ERROR);
	$new = $modify($old, $data);
	if ($new == $old)
	    break;
	$edited = wp_edit_page($title, $new, $summary, $token, $ctx, $last_ts);
	$tries++;
	if ($edited)
	    break;
	if (isset($max_tries) && $tries >= $max_tries)
	    break;
    }
}

function wp_edit_test($title, $modify, $data, $ctx) {
    $old = wp_get($title, $ctx);
    return $modify($old, $data);
}

/* Returns an associative array with the name of the image repository for each file
 * Status: stable */
function wp_locate_files($files, $context) {
    if (empty($files))
	return null;
    $post = array(
	"action" => "query",
	"format" => "php",
	"prop" => "imageinfo",
	"titles" => implode("|", $files),
    );
    if (isset($context) && array_key_exists("maxlag", $context))
	$post["maxlag"] = (string)$context["maxlag"];
    $ret = wp_post($post, $context);
    $ret = unserialize($ret);
    $pages = $ret["query"]["pages"];
    $info = array();
    foreach ($pages as $p)
	$info[$p["title"]] = $p["imagerepository"];
    return $info;
}

/* Returns a list of pages in the category
 * Status: stable */
function wp_get_category_members($category, $context = null) {
    $ctitle = $category;
    if (!eregi("^Category:", $category))
	$ctitle = "Category:".$category;
    $mlist = array();
    while (true) {
	$post = array(
	    "action" => "query",
	    "format" => "php",
	    "list" => "categorymembers",
	    "cmtitle" => $ctitle,
	);
	if (isset($context) && array_key_exists("maxlag", $context))
	    $post["maxlag"] = (string)$context["maxlag"];
	if (isset($context) && array_key_exists("qlimit", $context))
	    $post["cmlimit"] = (string)$context["qlimit"];
	if (isset($continue))
	    $post["cmcontinue"] = (string)$continue;
	$ret = wp_post($post, $context);
	$ret = unserialize($ret);
	if (array_key_exists("error", $ret)) {
	    trigger_error($ret["error"]["info"], E_USER_NOTICE);
	    return null;
	}
	$members = $ret["query"]["categorymembers"];
	foreach ($members as $m)
	    $mlist[] = $m["title"];
	if (array_key_exists("query-continue", $ret))
	    $continue = $ret["query-continue"]["categorymembers"]["cmcontinue"];
	else
	    break;
    }
    return $mlist;
}

/* Goes through the page history to find when subpages were transcluded. The list
 * function returns an array of transcluded subpages and takes the arguments:
 * list_fn($title, $contents);
 * Pass in an array of the subpages to look for as $current_tcs. */
function wp_transcluded_dates($title, $list_fn, $current_tcs, $context = null) {
    $tc_ts = array();
    $found_missing = array();
    while (true) {
	$post = array(
	    "action" => "query",
	    "format" => "php",
	    "prop" => "revisions",
	    "titles" => $title,
	    "rvprop" => "timestamp|content",
	);
	if (isset($context) && array_key_exists("qlimit", $context))
	    $post["rvlimit"] = (string)$context["qlimit"];
	if (isset($context) && array_key_exists("maxlag", $context))
	    $post["maxlag"] = (string)$context["maxlag"];
	if (isset($continue))
	    $post["rvstartid"] = (string)$continue;
	$ret = wp_post($post, $context);
	$ret = unserialize($ret);
	$pages = $ret["query"]["pages"];
	$revs = array();
	foreach ($pages as $p) {
	    if ($p["title"] != $title)
		continue;
	    $revs = $p["revisions"];
	    break;
	}
	foreach ($revs as $r) {
	    $time = strtotime($r["timestamp"]);
	    $tcs = $list_fn($title, $r["*"]);
	    if (empty($tcs))
		continue;
	    if (isset($current_tcs)) {
		foreach ($current_tcs as $tc) {
		    if (!in_array($tc, $tcs))
			$found_missing[$tc] = true;
		}
	    }
	    foreach ($tcs as $tc) {
		if (array_key_exists($tc, $found_missing))
		    continue;
		if (!array_key_exists($tc, $tc_ts))
		    $tc_ts[$tc] = time();
		if ($tc_ts[$tc] > $time)
		    $tc_ts[$tc] = $time;
	    }
	}
	if (isset($current_tcs)) {
	    $all_missing = true;
	    foreach ($current_tcs as $tc) {
		if (array_key_exists($tc, $found_missing))
		    continue;
		$all_missing = false;
		break;
	    }
	    if ($all_missing)
		break;
	}
	if (isset($ret["query-continue"]))
	    $continue = $ret["query-continue"]["revisions"]["rvstartid"];
	else
	    break;
    }
    return $tc_ts;
}
?>