<?php
if (!defined('APP_STARTED')) {
die('Forbidden!');
}
class Wiki
{
protected $_renderers = array(
'md' => 'Markdown',
'markdown' => 'Markdown',
'mdown' => 'Markdown',
'htm' => 'HTML',
'html' => 'HTML'
);
protected $_ignore = "/^\..*|^CVS$/"; // Match dotfiles and CVS
protected $_force_unignore = false; // always show these files (false to disable)
protected $_action;
protected $_default_page_data = array(
'title' => false, // will use APP_NAME by default
'description' => 'Wikitten is a small, fast, PHP wiki.',
'tags' => array('wikitten', 'wiki'),
'page' => ''
);
/**
* @param string $extension
* @return string|callable
*/
protected function _getRenderer($extension)
{
if (!isset($this->_renderers[$extension])) {
return false;
}
$renderer = $this->_renderers[$extension];
require_once __DIR__ . DIRECTORY_SEPARATOR . 'renderers' . DIRECTORY_SEPARATOR . "$renderer.php";
return $renderer;
}
protected function notFound($page){
$page = htmlspecialchars($page, ENT_QUOTES);
throw new Exception("Page '$page' was not found");
}
protected function _usePasteBin()
{
return defined('ENABLE_PASTEBIN') && ENABLE_PASTEBIN && defined('PASTEBIN_API_KEY') && PASTEBIN_API_KEY;
}
/**
* Given a file path, verifies if the file is safe to touch,
* given permissions, if it's within the library, etc.
*
* @param string $path
* @return bool
*/
protected function _pathIsSafe($path)
{
if ($path && strpos($path, LIBRARY) === 0) {
return true;
}
return false;
}
/**
* Given a string with a page's source, attempts to locate a
* section of JSON Front Matter in the heading, and returns
* the remaining source, and an array of extracted meta data.
*
* JSON Front Matter will only be considered when present
* within two lines consisting of three dashes:
*
* ---
* { "title": "hello world" }
* ---
*
* Additionally, the opening and closing brackets may be dropped,
* and this method will still interpret the content as a hash:
*
* ---
* "title": "hello, world",
* "tags": ["hello", "world"]
* ---
*
* @param string $source
* @return array array($remaining_source, $meta_data)
*/
protected function _extractJsonFrontMatter($source)
{
static $front_matter_regex = "/^---[\r\n](.*)[\r\n]---[\r\n](.*)/s";
$source = ltrim($source);
$meta_data = array();
if (preg_match($front_matter_regex, $source, $matches)) {
$json = trim($matches[1]);
$source = trim($matches[2]);
// Locate or append starting and ending brackets,
// if necessary. I lazily only check the first
// character for a bracket, so that it'll work
// even if the user includes a hash in the last
// line:
if ($json[0] != '{') {
$json = '{' . $json . '}';
}
// Decode & validate the JSON payload:
$meta_data = json_decode($json, true, 512);
// Check for errors:
if ($meta_data === null) {
$error = json_last_error();
$message = 'There was an error parsing the JSON Front Matter for this page';
// todo: Better error information?
if ($error == JSON_ERROR_SYNTAX) {
$message .= ': Incorrect JSON syntax (missing comma, or double-quotes?)';
}
throw new RuntimeException($message);
}
}
return array($source, $meta_data);
}
protected function _view($view, $variables = array())
{
extract($variables);
$content = __DIR__ . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . "$view.php";
if (!isset($layout)) {
$layout = __DIR__ . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'layout.php';
}
if (file_exists($content)) {
ob_start();
include($content);
$content = ob_get_contents();
ob_end_clean();
if ($layout) {
include $layout;
} else {
echo $content;
}
} else {
throw new Exception("View $view not found");
}
}
public function dispatch()
{
if (!function_exists("finfo_open")) {
die("<p>Please enable the PHP Extension <code style='background-color: #eee; border: 1px solid #ccc; padding: 3px; border-radius: 3px; line-height: 1;'>FileInfo.dll</code> by uncommenting or adding the following line:</p><pre style='background-color: #eee; border: 1px solid #ccc; padding: 5px; border-radius: 3px;'><code><span style='color: #999;'>;</span>extension=php_fileinfo.dll <span style='color: #999; margin-left: 25px;'># You can just uncomment by removing the semicolon (;) in the front.</span></code></pre>");
}
$action = $this->_getAction();
$actionMethod = "{$action}Action";
if ($action === null || !method_exists($this, $actionMethod)) {
$this->_404();
}
$this->$actionMethod();
}
protected function _getAction()
{
if (isset($_REQUEST['a'])) {
$action = $_REQUEST['a'];
if (in_array("{$action}Action", get_class_methods(get_class($this)))) {
$this->_action = $action;
}
} else {
$this->_action = 'index';
}
return $this->_action;
}
protected function _json($data = array())
{
header("Content-type: text/x-json");
echo(is_string($data) ? $data : json_encode($data));
exit();
}
protected function _isXMLHttpRequest()
{
if ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
return true;
}
if (function_exists('apache_request_headers')) {
$headers = apache_request_headers();
if ($headers['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
return true;
}
}
return false;
}
protected function _404($message = 'Page not found.')
{
header('HTTP/1.0 404 Not Found', true);
$page_data = $this->_default_page_data;
$page_data['title'] = 'Not Found';
$this->_view('uhoh', array(
'error' => $message,
'parts' => array('Uh-oh'),
'page' => $page_data
));
exit;
}
public function indexAction()
{
$request = parse_url($_SERVER['REQUEST_URI']);
$page = str_replace("###" . APP_DIR . "/", "", "###" . urldecode($request['path']));
if (!$page) {
if (file_exists(LIBRARY . DIRECTORY_SEPARATOR . DEFAULT_FILE)) {
$this->_render(DEFAULT_FILE);
return;
}
$this->_view('index', array(
'page' => $this->_default_page_data
));
return;
}
try {
$this->_render($page);
} catch (Exception $e) {
$this->_404($e->getMessage());
}
}
/**
* /?a=edit
* If ENABLE_EDITING is true, handles file editing through
* the web interface.
*/
public function editAction()
{
// Bail out early if editing isn't even enabled, or
// we don't get the right request method && params
// NOTE: $_POST['source'] may be empty if the user just deletes
// everything, but it should always be set.
if (!ENABLE_EDITING || $_SERVER['REQUEST_METHOD'] != 'POST'
|| empty($_POST['ref']) || !isset($_POST['source'])
) {
$this->_404();
}
$ref = $_POST['ref'];
$source = $_POST['source'];
$file = base64_decode($ref);
$path = realpath(LIBRARY . DIRECTORY_SEPARATOR . $file);
// Check if the file is safe to work with, otherwise just
// give back a generic 404 aswell, so we don't allow blind
// scanning of files:
// @todo: we CAN give back a more informative error message
// for files that aren't writable...
if (!$this->_pathIsSafe($path) && !is_writable($path)) {
$this->_404();
}
// Check if empty
if(trim($source)){
// Save the changes, and redirect back to the same page
file_put_contents($path, $source);
}else{
// Delete file and redirect too (but it will return 404)
unlink($path);
}
$redirect_url = BASE_URL . "/$file";
header("HTTP/1.0 302 Found", true);
header("Location: $redirect_url");
exit();
}
/**
* Handle createion of PasteBin pastes
* @return string JSON response
*/
public function createPasteBinAction()
{
if (!$this->_usePasteBin()) {
$this->_404();
}
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (isset($_POST['ref'])) {
$file = base64_decode($_POST['ref']);
$path = realpath(LIBRARY . DIRECTORY_SEPARATOR . $file);
if (!$this->_pathIsSafe($path)) {
$this->_404();
} else {
$content = file_get_contents($path);
$name = pathinfo($path, PATHINFO_BASENAME);
require_once PLUGINS . DIRECTORY_SEPARATOR . 'PasteBin.php';
$response = array();
$pastebin = new PasteBin(PASTEBIN_API_KEY);
/**
* @todo Add/improve autodetection of file format
*/
$url = $pastebin->createPaste($content, PasteBin::PASTE_PRIVACY_PUBLIC, $name, PasteBin::PASTE_EXPIRE_1W);
if ($url) {
$response['status'] = 'ok';
$response['url'] = $url;
} else {
$response['status'] = 'fail';
$response['error'] = $pastebin->getError();
}
header('Content-Type: application/json');
echo json_encode($response);
exit();
}
}
}
exit();
}
/**
* Singleton
* @return Wiki
*/
public static function instance()
{
static $instance;
if (!($instance instanceof self)) {
$instance = new self();
}
return $instance;
}
public function createAction()
{
$request = parse_url($_SERVER['REQUEST_URI']);
$page = str_replace("###" . APP_DIR . "/", "", "###" . urldecode($request['path']));
$filepath = LIBRARY . urldecode($request['path']);
$content = "# " . htmlspecialchars($page, ENT_QUOTES, 'UTF-8');
// if feature not enabled, go to 404
if (!ENABLE_EDITING || file_exists($filepath)) {
$this->_404();
}
// Create subdirectory recursively, if neccessary
mkdir(dirname($filepath), 0755, true);
// Save default content, and redirect back to the new page
file_put_contents($filepath, $content);
if (file_exists($filepath)) {
// Redirect to new page
$redirect_url = BASE_URL . "/$page";
header("HTTP/1.0 302 Found", true);
header("Location: $redirect_url");
exit();
} else {
$this->_404();
}
}
}