1432 lines
46 KiB
PHP
1432 lines
46 KiB
PHP
<?php
|
|
/**
|
|
* Class: MainController
|
|
* The logic controlling the blog.
|
|
*/
|
|
class MainController extends Controllers implements Controller {
|
|
# Array: $urls
|
|
# An array of clean URL => dirty URL translations.
|
|
public $urls = array(
|
|
'|/id/post/([0-9]+)/|'
|
|
=> '/?action=id&post=$1',
|
|
|
|
'|/id/page/([0-9]+)/|'
|
|
=> '/?action=id&page=$1',
|
|
|
|
'|/author/([0-9]+)/|'
|
|
=> '/?action=author&id=$1',
|
|
|
|
'|/random/([^/]+)/|'
|
|
=> '/?action=random&feather=$1',
|
|
|
|
'|/matter/([^/]+)/|'
|
|
=> '/?action=matter&url=$1',
|
|
|
|
'|/search/([^/]+)/|'
|
|
=> '/?action=search&query=$1',
|
|
|
|
'|/archive/([0-9]{4})/([0-9]{2})/([0-9]{2})/|'
|
|
=> '/?action=archive&year=$1&month=$2&day=$3',
|
|
|
|
'|/archive/([0-9]{4})/([0-9]{2})/|'
|
|
=> '/?action=archive&year=$1&month=$2',
|
|
|
|
'|/archive/([0-9]{4})/|'
|
|
=> '/?action=archive&year=$1',
|
|
|
|
'|/([^/]+)/feed/|'
|
|
=> '/?action=$1&feed'
|
|
);
|
|
|
|
# Variable: $twig
|
|
# Environment for the Twig template engine.
|
|
private $twig;
|
|
|
|
/**
|
|
* Function: __construct
|
|
* Loads the Twig parser and sets up the l10n domain.
|
|
*/
|
|
private function __construct() {
|
|
$loader = new \Twig\Loader\FilesystemLoader(THEME_DIR);
|
|
$config = Config::current();
|
|
$theme = Theme::current();
|
|
|
|
$this->twig = new \Twig\Environment(
|
|
$loader,
|
|
array(
|
|
"debug" => DEBUG,
|
|
"strict_variables" => DEBUG,
|
|
"charset" => "UTF-8",
|
|
"cache" => CACHES_DIR.DIR."twig",
|
|
"autoescape" => false)
|
|
);
|
|
|
|
$this->twig->addExtension(
|
|
new Leaf()
|
|
);
|
|
|
|
$this->twig->registerUndefinedFunctionCallback(
|
|
"twig_callback_missing_function"
|
|
);
|
|
|
|
$this->twig->registerUndefinedFilterCallback(
|
|
"twig_callback_missing_filter"
|
|
);
|
|
|
|
# Load the theme translator.
|
|
load_translator($theme->safename, THEME_DIR.DIR."locale");
|
|
|
|
# Set the limit for pagination.
|
|
$this->post_limit = $config->posts_per_page;
|
|
}
|
|
|
|
/**
|
|
* Function: parse
|
|
* Route constructor calls this to interpret clean URLs and determine the action.
|
|
*/
|
|
public function parse($route): ?string {
|
|
$config = Config::current();
|
|
|
|
# Serve the index if the first arg is empty and / is not a route.
|
|
if (empty($route->arg[0]) and !isset($config->routes["/"]))
|
|
return $route->action = "index";
|
|
|
|
# Serve the index if the first arg is a query and action is unset.
|
|
if (empty($route->action) and strpos($route->arg[0], "?") === 0)
|
|
return $route->action = "index";
|
|
|
|
# Discover feed requests.
|
|
if (
|
|
$route->action == "feed" or
|
|
preg_match("/\/feed\/?$/", $route->request)
|
|
)
|
|
$this->feed = true;
|
|
|
|
# Discover pagination.
|
|
if (
|
|
preg_match_all(
|
|
"/\/((([^_\/]+)_)?page)\/([0-9]+)/",
|
|
$route->request,
|
|
$pages
|
|
)
|
|
) {
|
|
foreach ($pages[1] as $index => $variable)
|
|
$_GET[$variable] = (int) $pages[4][$index];
|
|
|
|
# Looks like pagination of the index.
|
|
if ($route->arg[0] == $pages[1][0])
|
|
return $route->action = "index";
|
|
}
|
|
|
|
# Archive.
|
|
if ($route->arg[0] == "archive") {
|
|
# Make sure they're numeric; could be a "/page/" in there.
|
|
if (isset($route->arg[1]) and is_numeric($route->arg[1])) {
|
|
$_GET['year'] = $route->arg[1];
|
|
|
|
if (isset($route->arg[2]) and is_numeric($route->arg[2])) {
|
|
$_GET['month'] = $route->arg[2];
|
|
|
|
if (isset($route->arg[3]) and is_numeric($route->arg[3]))
|
|
$_GET['day'] = $route->arg[3];
|
|
}
|
|
}
|
|
|
|
return $route->action = "archive";
|
|
}
|
|
|
|
# Search.
|
|
if ($route->arg[0] == "search") {
|
|
if (isset($route->arg[1]))
|
|
$_GET['query'] = $route->arg[1];
|
|
|
|
return $route->action = "search";
|
|
}
|
|
|
|
# Author.
|
|
if ($route->arg[0] == "author") {
|
|
if (isset($route->arg[1]))
|
|
$_GET['id'] = $route->arg[1];
|
|
|
|
return $route->action = "author";
|
|
}
|
|
|
|
# Random.
|
|
if ($route->arg[0] == "random") {
|
|
if (isset($route->arg[1]))
|
|
$_GET['feather'] = $route->arg[1];
|
|
|
|
return $route->action = "random";
|
|
}
|
|
|
|
# Matter.
|
|
if ($route->arg[0] == "matter") {
|
|
if (isset($route->arg[1]))
|
|
$_GET['url'] = $route->arg[1];
|
|
|
|
return $route->action = "matter";
|
|
}
|
|
|
|
# Static ID of a post or page.
|
|
if ($route->arg[0] == "id") {
|
|
if (isset($route->arg[1]) and isset($route->arg[2])) {
|
|
switch ($route->arg[1]) {
|
|
case "post":
|
|
$_GET["post"] = $route->arg[2];
|
|
break;
|
|
case "page":
|
|
$_GET["page"] = $route->arg[2];
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $route->action = "id";
|
|
}
|
|
|
|
# Custom route?
|
|
$route->custom();
|
|
|
|
# Are we viewing a post?
|
|
Post::from_url(
|
|
$route->request,
|
|
$route,
|
|
array("drafts" => true)
|
|
);
|
|
|
|
# Are we viewing a page?
|
|
Page::from_url(
|
|
$route->request,
|
|
$route
|
|
);
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Function: exempt
|
|
* Route constructor calls this to determine "view_site" exemptions.
|
|
*/
|
|
public function exempt($action): bool {
|
|
$exemptions = array(
|
|
"login",
|
|
"logout",
|
|
"register",
|
|
"activate",
|
|
"lost_password",
|
|
"reset_password"
|
|
);
|
|
return in_array($action, $exemptions);
|
|
}
|
|
|
|
/**
|
|
* Function: main_index
|
|
* Grabs the posts for the main index.
|
|
*/
|
|
public function main_index(): void {
|
|
$this->display(
|
|
"pages".DIR."index",
|
|
array(
|
|
"posts" => new Paginator(
|
|
Post::find(array("placeholders" => true)),
|
|
$this->post_limit
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_updated
|
|
* Grabs the posts that have been updated.
|
|
*/
|
|
public function main_updated(): void {
|
|
$this->display(
|
|
array("pages".DIR."updated", "pages".DIR."index"),
|
|
array(
|
|
"posts" => new Paginator(
|
|
Post::find(
|
|
array(
|
|
"placeholders" => true,
|
|
"where" => array("updated_at >" => SQL_DATETIME_ZERO),
|
|
"order" => "updated_at DESC, created_at DESC, id DESC"
|
|
)
|
|
),
|
|
$this->post_limit
|
|
)
|
|
),
|
|
__("Updated posts")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_author
|
|
* Grabs the posts created by a user.
|
|
*/
|
|
public function main_author(): void {
|
|
if (empty($_GET['id']) or !is_numeric($_GET['id']))
|
|
Flash::warning(
|
|
__("You did not specify a user ID."),
|
|
"/"
|
|
);
|
|
|
|
$user = new User($_GET['id']);
|
|
|
|
if ($user->no_results)
|
|
Flash::warning(
|
|
__("User not found."),
|
|
"/"
|
|
);
|
|
|
|
$author = (object) array(
|
|
"id" => $user->id,
|
|
"name" => oneof($user->full_name, $user->login),
|
|
"website" => $user->website,
|
|
"email" => $user->email,
|
|
"joined" => $user->joined_at
|
|
);
|
|
|
|
$this->display(
|
|
array("pages".DIR."author", "pages".DIR."index"),
|
|
array(
|
|
"author" => $author,
|
|
"posts" => new Paginator(
|
|
Post::find(
|
|
array(
|
|
"placeholders" => true,
|
|
"where" => array("user_id" => $user->id)
|
|
)
|
|
),
|
|
$this->post_limit
|
|
)
|
|
),
|
|
_f("Posts created by “%s”", fix($author->name))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_archive
|
|
* Grabs the posts for the archive page.
|
|
*/
|
|
public function main_archive(): void {
|
|
$sql = SQL::current();
|
|
$statuses = Post::statuses();
|
|
$feathers = Post::feathers();
|
|
|
|
$months = array();
|
|
$posts = new Paginator(array());
|
|
|
|
fallback($_GET['year']);
|
|
fallback($_GET['month']);
|
|
fallback($_GET['day']);
|
|
|
|
# Default to either the year of the latest post or the current year.
|
|
if (!isset($_GET['year'])) {
|
|
$latest = $sql->select(
|
|
tables:"posts",
|
|
fields:"created_at",
|
|
conds:array($feathers, $statuses),
|
|
order:array("created_at DESC")
|
|
)->fetch();
|
|
|
|
$latest = ($latest === false) ?
|
|
time() :
|
|
$latest["created_at"] ;
|
|
}
|
|
|
|
$start = mktime(
|
|
0, 0, 0,
|
|
(
|
|
is_numeric($_GET['month']) ?
|
|
(int) $_GET['month'] :
|
|
1
|
|
),
|
|
(
|
|
is_numeric($_GET['day']) ?
|
|
(int) $_GET['day'] :
|
|
1
|
|
),
|
|
(
|
|
is_numeric($_GET['year']) ?
|
|
(int) $_GET['year'] :
|
|
(int) when("Y", $latest)
|
|
)
|
|
);
|
|
|
|
if (is_numeric($_GET['day'])) {
|
|
$depth = "day";
|
|
$limit = strtotime(
|
|
"tomorrow",
|
|
$start
|
|
);
|
|
$title = _f("Archive of %s", _w("d F Y", $start));
|
|
$posts = new Paginator(
|
|
Post::find(
|
|
array(
|
|
"placeholders" => true,
|
|
"where" => array(
|
|
"created_at LIKE" => when("Y-m-d%", $start)
|
|
),
|
|
"order" => "created_at DESC, id DESC"
|
|
)
|
|
),
|
|
$this->post_limit
|
|
);
|
|
} elseif (is_numeric($_GET['month'])) {
|
|
$depth = "month";
|
|
$limit = strtotime(
|
|
"midnight first day of next month",
|
|
$start
|
|
);
|
|
$title = _f("Archive of %s", _w("F Y", $start));
|
|
$posts = new Paginator(
|
|
Post::find(
|
|
array(
|
|
"placeholders" => true,
|
|
"where" => array(
|
|
"created_at LIKE" => when("Y-m-%", $start)
|
|
),
|
|
"order" => "created_at DESC, id DESC"
|
|
)
|
|
),
|
|
$this->post_limit
|
|
);
|
|
} else {
|
|
$depth = "year";
|
|
$limit = strtotime(
|
|
"midnight first day of next year",
|
|
$start
|
|
);
|
|
$title = _f("Archive of %s", _w("Y", $start));
|
|
|
|
$results = Post::find(
|
|
array(
|
|
"where" => array(
|
|
"created_at LIKE" => when("Y-%-%", $start)
|
|
),
|
|
"order" => "created_at DESC, id DESC"
|
|
)
|
|
);
|
|
|
|
foreach ($results as $result) {
|
|
$created_at = strtotime($result->created_at);
|
|
$this_month = strtotime(
|
|
"midnight first day of this month",
|
|
$created_at
|
|
);
|
|
|
|
if (!isset($months[$this_month]))
|
|
$months[$this_month] = array();
|
|
|
|
$months[$this_month][] = $result;
|
|
}
|
|
}
|
|
|
|
# Are there posts older than those displayed?
|
|
$next = $sql->select(
|
|
tables:"posts",
|
|
fields:"created_at",
|
|
conds:array(
|
|
"created_at <" => datetime($start),
|
|
$statuses,
|
|
$feathers
|
|
),
|
|
order:array("created_at DESC")
|
|
)->fetchColumn();
|
|
|
|
# Are there posts newer than those displayed?
|
|
$prev = $sql->select(
|
|
tables:"posts",
|
|
fields:"created_at",
|
|
conds:array(
|
|
"created_at >=" => datetime($limit),
|
|
$statuses,
|
|
$feathers
|
|
),
|
|
order:array("created_at ASC")
|
|
)->fetchColumn();
|
|
|
|
$prev = ($prev === false) ?
|
|
null :
|
|
strtotime($prev) ;
|
|
$next = ($next === false) ?
|
|
null :
|
|
strtotime($next) ;
|
|
|
|
$this->display(
|
|
"pages".DIR."archive",
|
|
array(
|
|
"posts" => $posts,
|
|
"months" => $months,
|
|
"archive" => array(
|
|
"when" => $start,
|
|
"depth" => $depth,
|
|
"next" => $next,
|
|
"prev" => $prev
|
|
)
|
|
),
|
|
$title
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_search
|
|
* Grabs the posts and pages for a search query.
|
|
*/
|
|
public function main_search(): void {
|
|
$config = Config::current();
|
|
$visitor = Visitor::current();
|
|
|
|
# Redirect searches to a clean URL or dirty GET depending on configuration.
|
|
if (isset($_POST['query']))
|
|
redirect(
|
|
"search/".
|
|
str_ireplace("%2F", "", urlencode($_POST['query'])).
|
|
"/"
|
|
);
|
|
|
|
if (!isset($_GET['query']) or $_GET['query'] == "")
|
|
Flash::warning(
|
|
__("Please enter a search term."),
|
|
"/"
|
|
);
|
|
|
|
list($where, $params) = keywords(
|
|
$_GET['query'],
|
|
"post_attributes.value LIKE :query OR url LIKE :query",
|
|
"posts"
|
|
);
|
|
|
|
$results = Post::find(
|
|
array(
|
|
"placeholders" => true,
|
|
"where" => $where,
|
|
"params" => $params
|
|
)
|
|
);
|
|
|
|
$ids = array();
|
|
|
|
foreach ($results[0] as $result)
|
|
$ids[] = $result["id"];
|
|
|
|
if (!empty($ids)) {
|
|
$posts = new Paginator(
|
|
Post::find(
|
|
array(
|
|
"placeholders" => true,
|
|
"where" => array("id" => $ids)
|
|
)
|
|
),
|
|
$this->post_limit
|
|
);
|
|
} else {
|
|
$posts = new Paginator(array());
|
|
}
|
|
|
|
if ($config->search_pages) {
|
|
list($where, $params) = keywords(
|
|
$_GET['query'],
|
|
"title LIKE :query OR body LIKE :query",
|
|
"pages"
|
|
);
|
|
|
|
if (!$visitor->group->can("view_page"))
|
|
$where["public"] = true;
|
|
|
|
$pages = Page::find(
|
|
array(
|
|
"where" => $where,
|
|
"params" => $params
|
|
)
|
|
);
|
|
} else {
|
|
$pages = array();
|
|
}
|
|
|
|
$this->display(
|
|
array("pages".DIR."search", "pages".DIR."index"),
|
|
array(
|
|
"posts" => $posts,
|
|
"pages" => $pages,
|
|
"search" => $_GET['query']
|
|
),
|
|
_f("Search results for “%s”", fix($_GET['query']))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_drafts
|
|
* Grabs the posts with draft status created by this user.
|
|
*/
|
|
public function main_drafts(): void {
|
|
$visitor = Visitor::current();
|
|
|
|
if (!$visitor->group->can("view_own_draft", "view_draft"))
|
|
show_403(
|
|
__("Access Denied"),
|
|
__("You do not have sufficient privileges to view drafts.")
|
|
);
|
|
|
|
$posts = new Paginator(
|
|
Post::find(
|
|
array(
|
|
"placeholders" => true,
|
|
"where" => array(
|
|
"status" => Post::STATUS_DRAFT,
|
|
"user_id" => $visitor->id)
|
|
)
|
|
),
|
|
$this->post_limit
|
|
);
|
|
|
|
$this->display(
|
|
array("pages".DIR."drafts", "pages".DIR."index"),
|
|
array("posts" => $posts),
|
|
__("Drafts")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_view
|
|
* Handles post viewing via dirty URL or clean URL.
|
|
* E.g. /year/month/day/url/.
|
|
*/
|
|
public function main_view($post = null): bool {
|
|
if (!isset($post))
|
|
$post = new Post(
|
|
array("url" => fallback($_GET['url'])),
|
|
array("drafts" => true)
|
|
);
|
|
|
|
if ($post->no_results)
|
|
return false;
|
|
|
|
if ($post->status == Post::STATUS_DRAFT)
|
|
Flash::message(
|
|
__("This post is not published.")
|
|
);
|
|
|
|
if ($post->status == Post::STATUS_SCHEDULED)
|
|
Flash::message(
|
|
__("This post is scheduled to be published.")
|
|
);
|
|
|
|
$this->display(
|
|
array("pages".DIR."view", "pages".DIR."index"),
|
|
array("post" => $post),
|
|
$post->title()
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Function: main_page
|
|
* Handles page viewing via dirty URL or clean URL.
|
|
* E.g. /parent/child/child-of-child/.
|
|
*/
|
|
public function main_page($page = null): bool {
|
|
$trigger = Trigger::current();
|
|
$visitor = Visitor::current();
|
|
|
|
if (!isset($page))
|
|
$page = new Page(
|
|
array("url" => fallback($_GET['url']))
|
|
);
|
|
|
|
if ($page->no_results)
|
|
return false;
|
|
|
|
if (
|
|
!$page->public and
|
|
!$visitor->group->can("view_page") and
|
|
$page->user_id != $visitor->id
|
|
) {
|
|
$trigger->call("can_not_view_page");
|
|
show_403(
|
|
__("Access Denied"),
|
|
__("You are not allowed to view this page.")
|
|
);
|
|
}
|
|
|
|
$this->display(
|
|
array("pages".DIR."page_".$page->url, "pages".DIR."page"),
|
|
array("page" => $page),
|
|
$page->title
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Function: main_id
|
|
* Views a post or page by its static ID.
|
|
*/
|
|
public function main_id(): bool {
|
|
if (!empty($_GET['post']) and is_numeric($_GET['post'])) {
|
|
$post = new Post($_GET['post']);
|
|
|
|
if ($post->no_results)
|
|
return false;
|
|
|
|
redirect($post->url());
|
|
}
|
|
|
|
if (!empty($_GET['page']) and is_numeric($_GET['page'])) {
|
|
$page = new Page($_GET['page']);
|
|
|
|
if ($page->no_results)
|
|
return false;
|
|
|
|
redirect($page->url());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Function: main_random
|
|
* Grabs a random post and redirects to it.
|
|
*/
|
|
public function main_random(): bool {
|
|
$conds = array(Post::statuses());
|
|
|
|
if (isset($_GET['feather']))
|
|
$conds["feather"] = preg_replace(
|
|
"|[^a-z_\-]|i",
|
|
"",
|
|
$_GET['feather']
|
|
);
|
|
else
|
|
$conds[] = Post::feathers();
|
|
|
|
$results = SQL::current()->select(
|
|
tables:"posts",
|
|
fields:"id",
|
|
conds:$conds
|
|
)->fetchAll();
|
|
|
|
if (!empty($results)) {
|
|
$ids = array();
|
|
|
|
foreach ($results as $result)
|
|
$ids[] = $result["id"];
|
|
|
|
shuffle($ids);
|
|
|
|
$post = new Post(reset($ids));
|
|
|
|
if ($post->no_results)
|
|
return false;
|
|
|
|
redirect($post->url());
|
|
}
|
|
|
|
Flash::warning(
|
|
__("There aren't enough posts for random selection."),
|
|
"/"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_matter
|
|
* Displays a standalone Twig template from the "pages" directory.
|
|
*/
|
|
public function main_matter(): bool {
|
|
$theme = Theme::current();
|
|
|
|
if (!isset($_GET['url']))
|
|
return false;
|
|
|
|
$matter = str_replace(array(DIR, "/"), "", $_GET['url']);
|
|
|
|
if ($matter == "")
|
|
return false;
|
|
|
|
$template = "pages".DIR."matter_".$matter;
|
|
|
|
if (!$theme->file_exists($template))
|
|
return false;
|
|
|
|
$this->display(
|
|
$template,
|
|
array(),
|
|
camelize($matter, true)
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Function: main_register
|
|
* Register a visitor as a new user.
|
|
*/
|
|
public function main_register(): void {
|
|
$config = Config::current();
|
|
|
|
if (!$config->can_register)
|
|
Flash::notice(
|
|
__("This site does not allow registration."),
|
|
"/"
|
|
);
|
|
|
|
if (logged_in())
|
|
Flash::notice(
|
|
__("You cannot register an account because you are already logged in."),
|
|
"/"
|
|
);
|
|
|
|
if (!empty($_POST)) {
|
|
if (!isset($_POST['hash']) or !Session::check_token($_POST['hash']))
|
|
Flash::warning(
|
|
__("Invalid authentication token.")
|
|
);
|
|
|
|
if (empty($_POST['login']) or derezz($_POST['login']))
|
|
Flash::warning(
|
|
__("Please enter a username for your account.")
|
|
);
|
|
|
|
$check = new User(
|
|
array("login" => $_POST['login'])
|
|
);
|
|
|
|
if (!$check->no_results)
|
|
Flash::warning(
|
|
__("That username is already in use.")
|
|
);
|
|
|
|
if (empty($_POST['password1']) or empty($_POST['password2']))
|
|
Flash::warning(
|
|
__("Passwords cannot be blank.")
|
|
);
|
|
elseif ($_POST['password1'] != $_POST['password2'])
|
|
Flash::warning(
|
|
__("Passwords do not match.")
|
|
);
|
|
elseif (password_strength($_POST['password1']) < 100)
|
|
Flash::message(
|
|
__("Please consider setting a stronger password for your account.")
|
|
);
|
|
|
|
if (empty($_POST['email']))
|
|
Flash::warning(
|
|
__("Email address cannot be blank.")
|
|
);
|
|
elseif (!is_email($_POST['email']))
|
|
Flash::warning(
|
|
__("Invalid email address.")
|
|
);
|
|
|
|
if (!check_captcha())
|
|
Flash::warning(
|
|
__("Incorrect captcha response.")
|
|
);
|
|
|
|
if (!empty($_POST['website']) and !is_url($_POST['website']))
|
|
Flash::warning(
|
|
__("Invalid website URL.")
|
|
);
|
|
|
|
if (!empty($_POST['website']))
|
|
$_POST['website'] = add_scheme($_POST['website']);
|
|
|
|
fallback($_POST['full_name'], "");
|
|
fallback($_POST['website'], "");
|
|
|
|
if (!Flash::exists("warning")) {
|
|
$user = User::add(
|
|
login:$_POST['login'],
|
|
password:User::hash_password($_POST['password1']),
|
|
email:$_POST['email'],
|
|
full_name:$_POST['full_name'],
|
|
website:$_POST['website'],
|
|
group_id:$config->default_group,
|
|
approved:($config->email_activation) ? false : true
|
|
);
|
|
|
|
if (!$user->approved) {
|
|
email_activate_account($user);
|
|
Flash::notice(
|
|
__("We have emailed you an activation link."),
|
|
"/"
|
|
);
|
|
}
|
|
|
|
Visitor::log_in($user);
|
|
|
|
Flash::notice(
|
|
__("Your account is now active."),
|
|
"/"
|
|
);
|
|
}
|
|
}
|
|
|
|
$this->display(
|
|
"forms".DIR."user".DIR."register",
|
|
array(),
|
|
__("Register")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_activate
|
|
* Activates (approves) a given login.
|
|
*/
|
|
public function main_activate()/*: never */{
|
|
if (logged_in())
|
|
Flash::notice(
|
|
__("You cannot activate an account because you are already logged in."),
|
|
"/"
|
|
);
|
|
|
|
fallback($_GET['login']);
|
|
fallback($_GET['token']);
|
|
|
|
$user = new User(
|
|
array("login" => $_GET['login'])
|
|
);
|
|
|
|
if ($user->no_results)
|
|
Flash::notice(
|
|
__("Please contact the blog administrator for help with your account."),
|
|
"/"
|
|
);
|
|
|
|
$hash = token($user->login);
|
|
|
|
if (!hash_equals($hash, $_GET['token']))
|
|
Flash::warning(
|
|
__("Invalid authentication token."),
|
|
"/"
|
|
);
|
|
|
|
if ($user->approved)
|
|
Flash::notice(
|
|
__("Your account has already been activated."),
|
|
"/"
|
|
);
|
|
|
|
$user = $user->update(approved:true);
|
|
|
|
Flash::notice(
|
|
__("Your account is now active."),
|
|
"login"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_login
|
|
* Logs in a user if they provide the username and password.
|
|
*/
|
|
public function main_login(): void {
|
|
$config = Config::current();
|
|
$trigger = Trigger::current();
|
|
|
|
if (logged_in())
|
|
Flash::notice(
|
|
__("You are already logged in."),
|
|
"/"
|
|
);
|
|
|
|
if (!empty($_POST)) {
|
|
if (!isset($_POST['hash']) or !Session::check_token($_POST['hash']))
|
|
Flash::warning(
|
|
__("Invalid authentication token.")
|
|
);
|
|
|
|
fallback($_POST['login']);
|
|
fallback($_POST['password']);
|
|
|
|
# You can block the login process by creating a Flash::warning().
|
|
$trigger->call("user_authenticate");
|
|
|
|
if (!User::authenticate($_POST['login'], $_POST['password']))
|
|
Flash::warning(
|
|
__("Incorrect username and/or password.")
|
|
);
|
|
|
|
if (!Flash::exists("warning")) {
|
|
$user = new User(
|
|
array("login" => $_POST['login'])
|
|
);
|
|
|
|
if (!$user->approved and $config->email_activation)
|
|
Flash::notice(
|
|
__("You must activate your account before you log in."),
|
|
"/"
|
|
);
|
|
|
|
Visitor::log_in($user);
|
|
|
|
Flash::notice(
|
|
__("Logged in."),
|
|
fallback($_SESSION['redirect_to'], "/")
|
|
);
|
|
}
|
|
}
|
|
|
|
$this->display(
|
|
"forms".DIR."user".DIR."login",
|
|
array(),
|
|
__("Log in")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_logout
|
|
* Logs out the current user.
|
|
*/
|
|
public function main_logout()/*: never */{
|
|
if (!logged_in())
|
|
Flash::notice(
|
|
__("You aren't logged in."),
|
|
"/"
|
|
);
|
|
|
|
Visitor::log_out();
|
|
|
|
Flash::notice(
|
|
__("Logged out."),
|
|
"/"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_controls
|
|
* Updates the current user when the form is submitted.
|
|
*/
|
|
public function main_controls(): void {
|
|
$visitor = Visitor::current();
|
|
|
|
if (!logged_in())
|
|
Flash::notice(
|
|
__("You must be logged in to access user controls."),
|
|
"login"
|
|
);
|
|
|
|
if (!empty($_POST)) {
|
|
if (!isset($_POST['hash']) or !Session::check_token($_POST['hash']))
|
|
Flash::warning(
|
|
__("Invalid authentication token.")
|
|
);
|
|
|
|
if (!empty($_POST['new_password1'])) {
|
|
if (
|
|
empty($_POST['new_password2']) or
|
|
$_POST['new_password1'] != $_POST['new_password2']
|
|
) {
|
|
Flash::warning(
|
|
__("Passwords do not match.")
|
|
);
|
|
} elseif (
|
|
password_strength($_POST['new_password1']) < 100
|
|
) {
|
|
Flash::message(
|
|
__("Please consider setting a stronger password for your account.")
|
|
);
|
|
}
|
|
}
|
|
|
|
if (empty($_POST['email']))
|
|
Flash::warning(
|
|
__("Email address cannot be blank.")
|
|
);
|
|
elseif (!is_email($_POST['email']))
|
|
Flash::warning(
|
|
__("Invalid email address.")
|
|
);
|
|
|
|
if (!empty($_POST['website']) and !is_url($_POST['website']))
|
|
Flash::warning(
|
|
__("Invalid website URL.")
|
|
);
|
|
|
|
if (!empty($_POST['website']))
|
|
$_POST['website'] = add_scheme($_POST['website']);
|
|
|
|
fallback($_POST['full_name'], "");
|
|
fallback($_POST['website'], "");
|
|
|
|
if (!Flash::exists("warning")) {
|
|
$password = (!empty($_POST['new_password1'])) ?
|
|
User::hash_password($_POST['new_password1']) :
|
|
$visitor->password ;
|
|
|
|
$visitor = $visitor->update(
|
|
login:$visitor->login,
|
|
password:$password,
|
|
email:$_POST['email'],
|
|
full_name:$_POST['full_name'],
|
|
website:$_POST['website'],
|
|
group_id:$visitor->group->id
|
|
);
|
|
|
|
Flash::notice(
|
|
__("Your profile has been updated."),
|
|
"/"
|
|
);
|
|
}
|
|
}
|
|
|
|
$this->display(
|
|
"forms".DIR."user".DIR."controls",
|
|
array(),
|
|
__("Controls")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_lost_password
|
|
* Emails a password reset link to the registered address of a user.
|
|
*/
|
|
public function main_lost_password(): void {
|
|
$config = Config::current();
|
|
|
|
if (logged_in())
|
|
Flash::notice(
|
|
__("You cannot reset your password because you are already logged in."),
|
|
"/"
|
|
);
|
|
|
|
if (!$config->email_correspondence)
|
|
Flash::notice(
|
|
__("Please contact the blog administrator for help with your account."),
|
|
"/"
|
|
);
|
|
|
|
if (!empty($_POST)) {
|
|
if (!isset($_POST['hash']) or !Session::check_token($_POST['hash']))
|
|
Flash::warning(
|
|
__("Invalid authentication token.")
|
|
);
|
|
|
|
if (empty($_POST['login']))
|
|
Flash::warning(
|
|
__("Please enter your username.")
|
|
);
|
|
|
|
if (!Flash::exists("warning")) {
|
|
$user = new User(
|
|
array("login" => $_POST['login'])
|
|
);
|
|
|
|
if (!$user->no_results)
|
|
email_reset_password($user);
|
|
|
|
Flash::notice(
|
|
__("We have emailed you a password reset link."),
|
|
"/"
|
|
);
|
|
}
|
|
}
|
|
|
|
$this->display(
|
|
"forms".DIR."user".DIR."lost_password",
|
|
array(),
|
|
__("Lost password")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_reset_password
|
|
* Resets the password for a given login.
|
|
*/
|
|
public function main_reset_password(): void {
|
|
$config = Config::current();
|
|
|
|
if (logged_in())
|
|
Flash::notice(
|
|
__("You cannot reset your password because you are already logged in."),
|
|
"/"
|
|
);
|
|
|
|
fallback($_REQUEST['issue']);
|
|
fallback($_REQUEST['login']);
|
|
fallback($_REQUEST['token']);
|
|
|
|
$age = time() - intval($_REQUEST['issue']);
|
|
|
|
if ($age > PASSWORD_RESET_TOKEN_LIFETIME)
|
|
Flash::notice(
|
|
__("The link has expired. Please try again."),
|
|
"lost_password"
|
|
);
|
|
|
|
$user = new User(
|
|
array("login" => $_REQUEST['login'])
|
|
);
|
|
|
|
if ($user->no_results)
|
|
Flash::notice(
|
|
__("Please contact the blog administrator for help with your account."),
|
|
"/"
|
|
);
|
|
|
|
$hash = token(array($_REQUEST['issue'], $user->login));
|
|
|
|
if (!hash_equals($hash, $_REQUEST['token']))
|
|
Flash::warning(
|
|
__("Invalid authentication token."),
|
|
"/"
|
|
);
|
|
|
|
if (!empty($_POST)) {
|
|
if (!isset($_POST['hash']) or !Session::check_token($_POST['hash']))
|
|
Flash::warning(
|
|
__("Invalid authentication token.")
|
|
);
|
|
|
|
if (empty($_POST['new_password1']) or empty($_POST['new_password2']))
|
|
Flash::warning(
|
|
__("Passwords cannot be blank.")
|
|
);
|
|
elseif ($_POST['new_password1'] != $_POST['new_password2'])
|
|
Flash::warning(
|
|
__("Passwords do not match.")
|
|
);
|
|
elseif (password_strength($_POST['new_password1']) < 100)
|
|
Flash::message(
|
|
__("Please consider setting a stronger password for your account.")
|
|
);
|
|
|
|
if (!Flash::exists("warning")) {
|
|
$user->update(
|
|
password:User::hash_password($_POST['new_password1'])
|
|
);
|
|
|
|
Flash::notice(
|
|
__("Your profile has been updated."),
|
|
"login"
|
|
);
|
|
}
|
|
}
|
|
|
|
$this->display(
|
|
"forms".DIR."user".DIR."reset_password",
|
|
array(
|
|
"issue" => $_REQUEST['issue'],
|
|
"login" => $_REQUEST['login'],
|
|
"token" => $_REQUEST['token']
|
|
),
|
|
__("Reset password")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_webmention
|
|
* Webmention receiver endpoint.
|
|
*/
|
|
public function main_webmention(): void {
|
|
webmention_receive(
|
|
fallback($_POST['source']),
|
|
fallback($_POST['target'])
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function: main_feed
|
|
* Grabs posts and serves a feed.
|
|
*/
|
|
public function main_feed($posts = null): void {
|
|
$config = Config::current();
|
|
$trigger = Trigger::current();
|
|
$theme = Theme::current();
|
|
|
|
# Fetch posts if we are being called as a responder.
|
|
if (!isset($posts)) {
|
|
$results = SQL::current()->select(
|
|
tables:"posts",
|
|
fields:"id",
|
|
conds:array("status" => Post::STATUS_PUBLIC),
|
|
order:array("id DESC"),
|
|
limit:$config->feed_items
|
|
)->fetchAll();
|
|
|
|
$ids = array();
|
|
|
|
foreach ($results as $result)
|
|
$ids[] = $result["id"];
|
|
|
|
if (!empty($ids)) {
|
|
$posts = Post::find(
|
|
array(
|
|
"where" => array("id" => $ids),
|
|
"order" => "created_at DESC, id DESC"
|
|
)
|
|
);
|
|
} else {
|
|
$posts = array();
|
|
}
|
|
}
|
|
|
|
if ($posts instanceof Paginator) {
|
|
$posts = $posts->reslice($config->feed_items);
|
|
$posts = $posts->paginated;
|
|
}
|
|
|
|
$latest_timestamp = 0;
|
|
|
|
foreach ($posts as $post) {
|
|
$created_at = strtotime($post->created_at);
|
|
|
|
if ($latest_timestamp < $created_at)
|
|
$latest_timestamp = $created_at;
|
|
}
|
|
|
|
if (strlen($theme->title)) {
|
|
$title = $theme->title;
|
|
$subtitle = "";
|
|
} else {
|
|
$title = $config->name;
|
|
$subtitle = $config->description;
|
|
}
|
|
|
|
$feed = new BlogFeed();
|
|
|
|
$feed->open(
|
|
title:$title,
|
|
subtitle:$subtitle,
|
|
updated:$latest_timestamp
|
|
);
|
|
|
|
foreach ($posts as $post) {
|
|
$updated = ($post->updated) ?
|
|
$post->updated_at :
|
|
$post->created_at ;
|
|
|
|
if (!$post->user->no_results) {
|
|
$author = oneof(
|
|
$post->user->full_name,
|
|
$post->user->login
|
|
);
|
|
$website = $post->user->website;
|
|
} else {
|
|
$author = null;
|
|
$website = null;
|
|
}
|
|
|
|
$feed->entry(
|
|
title:oneof($post->title(), $post->slug),
|
|
id:url("id/post/".$post->id),
|
|
content:$post->feed_content(),
|
|
link:$post->url(),
|
|
published:$post->created_at,
|
|
updated:$updated,
|
|
name:$author,
|
|
uri:$website
|
|
);
|
|
|
|
$trigger->call("feed_item", $post, $feed);
|
|
}
|
|
|
|
$feed->display();
|
|
}
|
|
|
|
/**
|
|
* Function: display
|
|
* Displays the page, or serves a feed if requested.
|
|
*
|
|
* Parameters:
|
|
* $template - The template file to display.
|
|
* $context - The context to be supplied to Twig.
|
|
* $title - The title for the page (optional).
|
|
* $pagination - <Paginator> instance (optional).
|
|
*
|
|
* Notes:
|
|
* $template is supplied sans ".twig" and relative to THEME_DIR.
|
|
* $template can be an array of fallback template filenames to try.
|
|
* $pagination will be inferred from the context if not supplied.
|
|
*/
|
|
public function display(
|
|
$template,
|
|
$context = array(),
|
|
$title = "",
|
|
$pagination = null
|
|
): void {
|
|
$config = Config::current();
|
|
$route = Route::current();
|
|
$trigger = Trigger::current();
|
|
$theme = Theme::current();
|
|
|
|
if ($this->displayed == true)
|
|
return;
|
|
|
|
# Try fallback template filenames.
|
|
if (is_array($template)) {
|
|
foreach (array_values($template) as $index => $try) {
|
|
if (
|
|
$theme->file_exists($try) or
|
|
($index + 1) == count($template)
|
|
) {
|
|
$this->display($try, $context, $title);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->displayed = true;
|
|
|
|
# Discover pagination in the context.
|
|
if (!isset($pagination)) {
|
|
foreach ($context as $item) {
|
|
if ($item instanceof Paginator) {
|
|
$pagination = $item;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
# Populate the theme title attribute.
|
|
$theme->title = $title;
|
|
|
|
# Serve a feed request if detected for this action.
|
|
if ($this->feed) {
|
|
if ($trigger->exists($route->action."_feed")) {
|
|
$trigger->call($route->action."_feed", $context);
|
|
return;
|
|
}
|
|
|
|
if (isset($context["posts"])) {
|
|
$this->main_feed($context["posts"]);
|
|
return;
|
|
}
|
|
}
|
|
|
|
$this->context = array_merge($context, $this->context);
|
|
$this->context["ip"] = $_SERVER['REMOTE_ADDR'];
|
|
$this->context["DIR"] = DIR;
|
|
$this->context["version"] = CHYRP_VERSION;
|
|
$this->context["codename"] = CHYRP_CODENAME;
|
|
$this->context["debug"] = DEBUG;
|
|
$this->context["now"] = time();
|
|
$this->context["site"] = $config;
|
|
$this->context["flash"] = Flash::current();
|
|
$this->context["theme"] = $theme;
|
|
$this->context["trigger"] = $trigger;
|
|
$this->context["route"] = $route;
|
|
$this->context["visitor"] = Visitor::current();
|
|
$this->context["visitor"]->logged_in = logged_in();
|
|
$this->context["title"] = $title;
|
|
$this->context["pagination"] = $pagination;
|
|
$this->context["modules"] = Modules::$instances;
|
|
$this->context["feathers"] = Feathers::$instances;
|
|
$this->context["POST"] = $_POST;
|
|
$this->context["GET"] = $_GET;
|
|
$this->context["sql_queries"] =& SQL::current()->queries;
|
|
$this->context["sql_debug"] =& SQL::current()->debug;
|
|
|
|
$trigger->filter($this->context, "twig_context_main");
|
|
$this->twig->display($template.".twig", $this->context);
|
|
}
|
|
|
|
/**
|
|
* Function: current
|
|
* Returns a singleton reference to the current class.
|
|
*/
|
|
public static function & current(): self {
|
|
static $instance = null;
|
|
$instance = (empty($instance)) ? new self() : $instance ;
|
|
return $instance;
|
|
}
|
|
}
|