<?php
/**
* @brief Dotclear core class
*
* True to its name dcCore is the core of Dotclear. It handles everything related
* to blogs, database connection, plugins...
*
* @package Dotclear
* @subpackage Core
*
* @copyright Olivier Meunier & Association Dotclear
* @copyright GPL-2.0-only
*/
class dcCore
{
public $con; ///< <b>connection</b> Database connection object
public $prefix; ///< <b>string</b> Database tables prefix
public $blog; ///< <b>dcBlog</b> dcBlog object
public $error; ///< <b>dcError</b> dcError object
public $auth; ///< <b>dcAuth</b> dcAuth object
public $session; ///< <b>sessionDB</b> sessionDB object
public $url; ///< <b>urlHandler</b> urlHandler object
public $wiki2xhtml; ///< <b>wiki2xhtml</b> wiki2xhtml object
public $plugins; ///< <b>dcModules</b> dcModules object
public $themes; ///< <b>dcThemes</b> dcThemes object
public $media; ///< <b>dcMedia</b> dcMedia object
public $postmedia; ///< <b>dcPostMedia</b> dcPostMedia object
public $rest; ///< <b>dcRestServer</b> dcRestServer object
public $log; ///< <b>dcLog</b> dcLog object
public $stime; ///< <b>float</b> starting time
public $meta; ///< <b>dcMeta</b> dcMeta object
public $adminurl; ///< <b>dcAdminURL</b> dcAdminURL object
public $notices; ///< <b>dcAdminNotices</b> dcNotices object
public $favs; ///< <b>dcFavorites</b> dcFavorites object
private $versions = null;
private $formaters = [];
private $behaviors = [];
private $post_types = [];
/**
* dcCore constructor inits everything related to Dotclear. It takes arguments
* to init database connection.
*
* @param string $driver The db driver
* @param string $host The db host
* @param string $db The db name
* @param string $user The db user
* @param string $password The db password
* @param string $prefix The tables prefix
* @param bool $persist Persistent database connection
*/
public function __construct($driver, $host, $db, $user, $password, $prefix, $persist)
{
if (defined('DC_START_TIME')) {
$this->stime = DC_START_TIME;
} else {
$this->stime = microtime(true);
}
$this->con = dbLayer::init($driver, $host, $db, $user, $password, $persist);
# define weak_locks for mysql
if ($this->con instanceof mysqliConnection) {
mysqliConnection::$weak_locks = true;
} elseif ($this->con instanceof mysqlimb4Connection) {
mysqlimb4Connection::$weak_locks = true;
}
# define searchpath for postgresql
if ($this->con instanceof pgsqlConnection) {
$searchpath = explode('.', $prefix, 2);
if (count($searchpath) > 1) {
$prefix = $searchpath[1];
$sql = 'SET search_path TO ' . $searchpath[0] . ',public;';
$this->con->execute($sql);
}
}
$this->prefix = $prefix;
$ttl = DC_SESSION_TTL;
if (!is_null($ttl)) { // @phpstan-ignore-line
if (substr(trim((string) $ttl), 0, 1) != '-') {
// Clearbricks requires negative session TTL
$ttl = '-' . trim((string) $ttl);
}
}
$this->error = new dcError();
$this->auth = $this->authInstance();
$this->session = new sessionDB($this->con, $this->prefix . 'session', DC_SESSION_NAME, '', null, DC_ADMIN_SSL, $ttl);
$this->url = new dcUrlHandlers();
$this->plugins = new dcPlugins($this);
$this->rest = new dcRestServer($this);
$this->meta = new dcMeta($this);
$this->log = new dcLog($this);
}
private function authInstance()
{
# You can set DC_AUTH_CLASS to whatever you want.
# Your new class *should* inherits dcAuth.
if (!defined('DC_AUTH_CLASS')) {
$c = 'dcAuth';
} else {
$c = DC_AUTH_CLASS;
}
if (!class_exists($c)) {
throw new Exception('Authentication class ' . $c . ' does not exist.');
}
if ($c != 'dcAuth' && !is_subclass_of($c, 'dcAuth')) {
throw new Exception('Authentication class ' . $c . ' does not inherit dcAuth.');
}
return new $c($this);
}
/// @name Blog init methods
//@{
/**
* Sets the blog to use.
*
* @param string $id The blog ID
*/
public function setBlog($id)
{
$this->blog = new dcBlog($this, $id);
}
/**
* Unsets blog property
*/
public function unsetBlog()
{
$this->blog = null;
}
//@}
/// @name Blog status methods
//@{
/**
* Gets all blog status.
*
* @return array An array of available blog status codes and names.
*/
public function getAllBlogStatus()
{
return [
1 => __('online'),
0 => __('offline'),
-1 => __('removed'),
];
}
/**
* Returns a blog status name given to a code. This is intended to be
* human-readable and will be translated, so never use it for tests.
* If status code does not exist, returns <i>offline</i>.
*
* @param integer $s Status code
*
* @return string The blog status name.
*/
public function getBlogStatus($s)
{
$r = $this->getAllBlogStatus();
if (isset($r[$s])) {
return $r[$s];
}
return $r[0];
}
//@}
/// @name Admin nonce secret methods
//@{
/**
* Gets the nonce.
*
* @return string The nonce.
*/
public function getNonce()
{
return $this->auth->cryptLegacy(session_id());
}
/**
* Check the nonce
*
* @param string $secret The nonce
*
* @return bool
*/
public function checkNonce($secret)
{
// 40 alphanumeric characters min
if (!preg_match('/^([0-9a-f]{40,})$/i', $secret)) {
return false;
}
return $secret == $this->auth->cryptLegacy(session_id());
}
/**
* Get the nonce HTML code
*
* @return mixed
*/
public function formNonce()
{
if (!session_id()) {
return;
}
return form::hidden(['xd_check'], $this->getNonce());
}
//@}
/// @name Text Formatters methods
//@{
/**
* Adds a new text formater which will call the function <var>$func</var> to
* transform text. The function must be a valid callback and takes one
* argument: the string to transform. It returns the transformed string.
*
* @param string $editor_id The editor identifier (dcLegacyEditor, dcCKEditor, ...)
* @param string $name The formater name
* @param callable $func The function to use, must be a valid and callable callback
*/
public function addEditorFormater($editor_id, $name, $func)
{
if (is_callable($func)) {
$this->formaters[$editor_id][$name] = $func;
}
}
/// @name Text Formatters methods
//@{
/**
Adds a new text formater which will call the function <var>$func</var> to
transform text. The function must be a valid callback and takes one
argument: the string to transform. It returns the transformed string.
@param name <b>string</b> Formater name
@param func <b>callback</b> Function to use, must be a valid and callable callback
*/
/**
* Adds a new dcLegacyEditor text formater which will call the function
* <var>$func</var> to transform text. The function must be a valid callback
* and takes one argument: the string to transform. It returns the transformed string.
*
* @param string $name The formater name
* @param callable $func The function to use, must be a valid and callable callback
*/
public function addFormater($name, $func)
{
$this->addEditorFormater('dcLegacyEditor', $name, $func);
}
/**
* Gets the editors list.
*
* @return array The editors.
*/
public function getEditors()
{
$editors = [];
foreach (array_keys($this->formaters) as $editor_id) {
$editors[$editor_id] = $this->plugins->moduleInfo($editor_id, 'name');
}
return $editors;
}
/**
Returns formaters list by editor
@param editor_id <b>string</b> Editor id
@return <b>array</b> An array of formaters names in values.
/**
*/
/**
* Gets the formaters.
*
* if @param editor_id is empty:
* return all formaters sorted by actives editors
*
* if @param editor_id is not empty
* return formaters for an editor if editor is active
* return empty() array if editor is not active.
* It can happens when a user choose an editor and admin deactivate that editor later
*
* @param string $editor_id The editor identifier (dcLegacyEditor, dcCKEditor, ...)
*
* @return array The formaters.
*/
public function getFormaters($editor_id = '')
{
$formaters_list = [];
if (!empty($editor_id)) {
if (isset($this->formaters[$editor_id])) {
$formaters_list = array_keys($this->formaters[$editor_id]);
}
} else {
foreach ($this->formaters as $editor => $formaters) {
$formaters_list[$editor] = array_keys($formaters);
}
}
return $formaters_list;
}
/**
* If <var>$name</var> is a valid formater, it returns <var>$str</var>
* transformed using that formater.
*
* @param string $editor_id The editor identifier (dcLegacyEditor, dcCKEditor, ...)
* @param string $name The formater name
* @param string $str The string to transform
*
* @return string
*/
public function callEditorFormater($editor_id, $name, $str)
{
if (isset($this->formaters[$editor_id]) && isset($this->formaters[$editor_id][$name])) {
return call_user_func($this->formaters[$editor_id][$name], $str);
}
// Fallback with another editor if possible
foreach ($this->formaters as $editor => $formaters) {
if (array_key_exists($name, $formaters)) {
return call_user_func($this->formaters[$editor][$name], $str);
}
}
return $str;
}
//@}
/**
* If <var>$name</var> is a valid dcLegacyEditor formater, it returns
* <var>$str</var> transformed using that formater.
*
* @param string $name The name
* @param string $str The string
*
* @return string
*/
public function callFormater($name, $str)
{
return $this->callEditorFormater('dcLegacyEditor', $name, $str);
}
//@}
/// @name Behaviors methods
//@{
/**
* Adds a new behavior to behaviors stack. <var>$func</var> must be a valid
* and callable callback.
*
* @param string $behavior The behavior
* @param callable $func The function
*/
public function addBehavior($behavior, $func)
{
if (is_callable($func)) {
$this->behaviors[$behavior][] = $func;
}
}
/**
* Determines if behavior exists in behaviors stack.
*
* @param string $behavior The behavior
*
* @return bool True if behavior exists, False otherwise.
*/
public function hasBehavior($behavior)
{
return isset($this->behaviors[$behavior]);
}
/**
* Gets the behaviors stack (or part of).
*
* @param string $behavior The behavior
*
* @return mixed The behaviors.
*/
public function getBehaviors($behavior = '')
{
if (empty($this->behaviors)) {
return;
}
if ($behavior == '') {
return $this->behaviors;
} elseif (isset($this->behaviors[$behavior])) {
return $this->behaviors[$behavior];
}
return [];
}
/**
* Calls every function in behaviors stack for a given behavior and returns
* concatened result of each function.
*
* Every parameters added after <var>$behavior</var> will be pass to
* behavior calls.
*
* @param string $behavior The behavior
* @param mixed ...$args The arguments
*
* @return mixed Behavior concatened result
*/
public function callBehavior($behavior, ...$args)
{
if (isset($this->behaviors[$behavior])) {
$res = '';
foreach ($this->behaviors[$behavior] as $f) {
$res .= call_user_func_array($f, $args);
}
return $res;
}
}
//@}
/// @name Post types URLs management
//@{
/**
* Gets the post admin url.
*
* @param string $type The type
* @param mixed $post_id The post identifier
* @param bool $escaped Escape the URL
*
* @return string The post admin url.
*/
public function getPostAdminURL($type, $post_id, $escaped = true)
{
if (!isset($this->post_types[$type])) {
$type = 'post';
}
$url = sprintf($this->post_types[$type]['admin_url'], $post_id);
return $escaped ? html::escapeURL($url) : $url;
}
/**
* Gets the post public url.
*
* @param string $type The type
* @param string $post_url The post url
* @param bool $escaped Escape the URL
*
* @return string The post public url.
*/
public function getPostPublicURL($type, $post_url, $escaped = true)
{
if (!isset($this->post_types[$type])) {
$type = 'post';
}
$url = sprintf($this->post_types[$type]['public_url'], $post_url);
return $escaped ? html::escapeURL($url) : $url;
}
/**
* Sets the post type.
*
* @param string $type The type
* @param string $admin_url The admin url
* @param string $public_url The public url
* @param string $label The label
*/
public function setPostType($type, $admin_url, $public_url, $label = '')
{
$this->post_types[$type] = [
'admin_url' => $admin_url,
'public_url' => $public_url,
'label' => ($label != '' ? $label : $type),
];
}
/**
* Gets the post types.
*
* @return array The post types.
*/
public function getPostTypes()
{
return $this->post_types;
}
//@}
/// @name Versions management methods
//@{
/**
* Gets the version of a module.
*
* @param string $module The module
*
* @return mixed The version.
*/
public function getVersion($module = 'core')
{
# Fetch versions if needed
if (!is_array($this->versions)) {
$strReq = 'SELECT module, version FROM ' . $this->prefix . 'version';
$rs = $this->con->select($strReq);
while ($rs->fetch()) {
$this->versions[$rs->module] = $rs->version;
}
}
if (isset($this->versions[$module])) {
return $this->versions[$module];
}
}
/**
* Sets the version of a module.
*
* @param string $module The module
* @param string $version The version
*/
public function setVersion($module, $version)
{
$cur_version = $this->getVersion($module);
$cur = $this->con->openCursor($this->prefix . 'version');
$cur->module = (string) $module;
$cur->version = (string) $version;
if ($cur_version === null) {
$cur->insert();
} else {
$cur->update("WHERE module='" . $this->con->escape($module) . "'");
}
$this->versions[$module] = $version;
}
/**
* Remove a module version entry
*
* @param string $module The module
*/
public function delVersion($module)
{
$strReq = 'DELETE FROM ' . $this->prefix . 'version ' .
"WHERE module = '" . $this->con->escape($module) . "' ";
$this->con->execute($strReq);
if (is_array($this->versions)) {
unset($this->versions[$module]);
}
}
//@}
/// @name Users management methods
//@{
/**
* Gets the user by its ID.
*
* @param string $id The identifier
*
* @return record The user.
*/
public function getUser($id)
{
$params['user_id'] = $id;
return $this->getUsers($params);
}
/**
* Returns a users list. <b>$params</b> is an array with the following
* optionnal parameters:
*
* - <var>q</var>: search string (on user_id, user_name, user_firstname)
* - <var>user_id</var>: user ID
* - <var>order</var>: ORDER BY clause (default: user_id ASC)
* - <var>limit</var>: LIMIT clause (should be an array ![limit,offset])
*
* @param array|ArrayObject $params The parameters
* @param bool $count_only Count only results
*
* @return record The users.
*/
public function getUsers($params = [], $count_only = false)
{
if ($count_only) {
$strReq = 'SELECT count(U.user_id) ' .
'FROM ' . $this->prefix . 'user U ' .
'WHERE NULL IS NULL ';
} else {
$strReq = 'SELECT U.user_id,user_super,user_status,user_pwd,user_change_pwd,' .
'user_name,user_firstname,user_displayname,user_email,user_url,' .
'user_desc, user_lang,user_tz, user_post_status,user_options, ' .
'count(P.post_id) AS nb_post ';
if (!empty($params['columns'])) {
$strReq .= ',';
if (is_array($params['columns'])) {
$strReq .= implode(',', $params['columns']);
} else {
$strReq .= $params['columns'];
}
$strReq .= ' ';
}
$strReq .= 'FROM ' . $this->prefix . 'user U ' .
'LEFT JOIN ' . $this->prefix . 'post P ON U.user_id = P.user_id ' .
'WHERE NULL IS NULL ';
}
if (!empty($params['q'])) {
$q = $this->con->escape(str_replace('*', '%', strtolower($params['q'])));
$strReq .= 'AND (' .
"LOWER(U.user_id) LIKE '" . $q . "' " .
"OR LOWER(user_name) LIKE '" . $q . "' " .
"OR LOWER(user_firstname) LIKE '" . $q . "' " .
') ';
}
if (!empty($params['user_id'])) {
$strReq .= "AND U.user_id = '" . $this->con->escape($params['user_id']) . "' ";
}
if (!$count_only) {
$strReq .= 'GROUP BY U.user_id,user_super,user_status,user_pwd,user_change_pwd,' .
'user_name,user_firstname,user_displayname,user_email,user_url,' .
'user_desc, user_lang,user_tz,user_post_status,user_options ';
if (!empty($params['order'])) {
if (preg_match('`^([^. ]+) (?:asc|desc)`i', $params['order'], $matches)) {
if (in_array($matches[1], ['user_id', 'user_name', 'user_firstname', 'user_displayname'])) {
$table_prefix = 'U.';
} else {
$table_prefix = ''; // order = nb_post (asc|desc)
}
$strReq .= 'ORDER BY ' . $table_prefix . $this->con->escape($params['order']) . ' ';
} else {
$strReq .= 'ORDER BY ' . $this->con->escape($params['order']) . ' ';
}
} else {
$strReq .= 'ORDER BY U.user_id ASC ';
}
}
if (!$count_only && !empty($params['limit'])) {
$strReq .= $this->con->limit($params['limit']);
}
$rs = $this->con->select($strReq);
$rs->extend('rsExtUser');
return $rs;
}
/**
* Adds a new user. Takes a cursor as input and returns the new user ID.
*
* @param cursor $cur The user cursor
*
* @throws Exception
*
* @return string
*/
public function addUser($cur)
{
if (!$this->auth->isSuperAdmin()) {
throw new Exception(__('You are not an administrator'));
}
if ($cur->user_id == '') {
throw new Exception(__('No user ID given'));
}
if ($cur->user_pwd == '') {
throw new Exception(__('No password given'));
}
$this->getUserCursor($cur);
if ($cur->user_creadt === null) {
$cur->user_creadt = date('Y-m-d H:i:s');
}
$cur->insert();
$this->auth->afterAddUser($cur);
return $cur->user_id;
}
/**
* Updates an existing user. Returns the user ID.
*
* @param string $id The user identifier
* @param cursor $cur The cursor
*
* @throws Exception
*
* @return string
*/
public function updUser($id, $cur)
{
$this->getUserCursor($cur);
if (($cur->user_id !== null || $id != $this->auth->userID()) && !$this->auth->isSuperAdmin()) {
throw new Exception(__('You are not an administrator'));
}
$cur->update("WHERE user_id = '" . $this->con->escape($id) . "' ");
$this->auth->afterUpdUser($id, $cur);
if ($cur->user_id !== null) {
$id = $cur->user_id;
}
# Updating all user's blogs
$rs = $this->con->select(
'SELECT DISTINCT(blog_id) FROM ' . $this->prefix . 'post ' .
"WHERE user_id = '" . $this->con->escape($id) . "' "
);
while ($rs->fetch()) {
$b = new dcBlog($this, $rs->blog_id);
$b->triggerBlog();
unset($b);
}
return $id;
}
/**
* Deletes a user.
*
* @param string $id The user identifier
*
* @throws Exception
*/
public function delUser($id)
{
if (!$this->auth->isSuperAdmin()) {
throw new Exception(__('You are not an administrator'));
}
if ($id == $this->auth->userID()) {
return;
}
$rs = $this->getUser($id);
if ($rs->nb_post > 0) {
return;
}
$strReq = 'DELETE FROM ' . $this->prefix . 'user ' .
"WHERE user_id = '" . $this->con->escape($id) . "' ";
$this->con->execute($strReq);
$this->auth->afterDelUser($id);
}
/**
* Determines if user exists.
*
* @param string $id The identifier
*
* @return bool True if user exists, False otherwise.
*/
public function userExists($id)
{
$strReq = 'SELECT user_id ' .
'FROM ' . $this->prefix . 'user ' .
"WHERE user_id = '" . $this->con->escape($id) . "' ";
$rs = $this->con->select($strReq);
return !$rs->isEmpty();
}
/**
* Returns all user permissions as an array which looks like:
*
* - [blog_id]
* - [name] => Blog name
* - [url] => Blog URL
* - [p]
* - [permission] => true
* - ...
*
* @param string $id The user identifier
*
* @return array The user permissions.
*/
public function getUserPermissions($id)
{
$strReq = 'SELECT B.blog_id, blog_name, blog_url, permissions ' .
'FROM ' . $this->prefix . 'permissions P ' .
'INNER JOIN ' . $this->prefix . 'blog B ON P.blog_id = B.blog_id ' .
"WHERE user_id = '" . $this->con->escape($id) . "' ";
$rs = $this->con->select($strReq);
$res = [];
while ($rs->fetch()) {
$res[$rs->blog_id] = [
'name' => $rs->blog_name,
'url' => $rs->blog_url,
'p' => $this->auth->parsePermissions($rs->permissions),
];
}
return $res;
}
/**
* Sets user permissions. The <var>$perms</var> array looks like:
*
* - [blog_id] => '|perm1|perm2|'
* - ...
*
* @param string $id The user identifier
* @param array $perms The permissions
*
* @throws Exception
*/
public function setUserPermissions($id, $perms)
{
if (!$this->auth->isSuperAdmin()) {
throw new Exception(__('You are not an administrator'));
}
$strReq = 'DELETE FROM ' . $this->prefix . 'permissions ' .
"WHERE user_id = '" . $this->con->escape($id) . "' ";
$this->con->execute($strReq);
foreach ($perms as $blog_id => $p) {
$this->setUserBlogPermissions($id, $blog_id, $p, false);
}
}
/**
* Sets the user blog permissions.
*
* @param string $id The user identifier
* @param string $blog_id The blog identifier
* @param array $perms The permissions
* @param bool $delete_first Delete permissions first
*
* @throws Exception (description)
*/
public function setUserBlogPermissions($id, $blog_id, $perms, $delete_first = true)
{
if (!$this->auth->isSuperAdmin()) {
throw new Exception(__('You are not an administrator'));
}
$no_perm = empty($perms);
$perms = '|' . implode('|', array_keys($perms)) . '|';
$cur = $this->con->openCursor($this->prefix . 'permissions');
$cur->user_id = (string) $id;
$cur->blog_id = (string) $blog_id;
$cur->permissions = $perms;
if ($delete_first || $no_perm) {
$strReq = 'DELETE FROM ' . $this->prefix . 'permissions ' .
"WHERE blog_id = '" . $this->con->escape($blog_id) . "' " .
"AND user_id = '" . $this->con->escape($id) . "' ";
$this->con->execute($strReq);
}
if (!$no_perm) {
$cur->insert();
}
}
/**
* Sets the user default blog. This blog will be selected when user log in.
*
* @param string $id The user identifier
* @param string $blog_id The blog identifier
*/
public function setUserDefaultBlog($id, $blog_id)
{
$cur = $this->con->openCursor($this->prefix . 'user');
$cur->user_default_blog = (string) $blog_id;
$cur->update("WHERE user_id = '" . $this->con->escape($id) . "'");
}
/**
* Gets the user cursor.
*
* @param cursor $cur The user cursor
*
* @throws Exception
*/
private function getUserCursor($cur)
{
if ($cur->isField('user_id')
&& !preg_match('/^[A-Za-z0-9@._-]{2,}$/', $cur->user_id)) {
throw new Exception(__('User ID must contain at least 2 characters using letters, numbers or symbols.'));
}
if ($cur->user_url !== null && $cur->user_url != '') {
if (!preg_match('|^http(s?)://|', $cur->user_url)) {
$cur->user_url = 'http://' . $cur->user_url;
}
}
if ($cur->isField('user_pwd')) {
if (strlen($cur->user_pwd) < 6) {
throw new Exception(__('Password must contain at least 6 characters.'));
}
$cur->user_pwd = $this->auth->crypt($cur->user_pwd);
}
if ($cur->user_lang !== null && !preg_match('/^[a-z]{2}(-[a-z]{2})?$/', $cur->user_lang)) {
throw new Exception(__('Invalid user language code'));
}
if ($cur->user_upddt === null) {
$cur->user_upddt = date('Y-m-d H:i:s');
}
if ($cur->user_options !== null) {
$cur->user_options = serialize((array) $cur->user_options);
}
}
/**
* Returns user default settings in an associative array with setting names in keys.
*
* @return array
*/
public function userDefaults()
{
return [
'edit_size' => 24,
'enable_wysiwyg' => true,
'toolbar_bottom' => false,
'editor' => ['xhtml' => 'dcCKEditor', 'wiki' => 'dcLegacyEditor'],
'post_format' => 'xhtml',
];
}
//@}
/// @name Blog management methods
//@{
/**
* Returns all blog permissions (users) as an array which looks like:
*
* - [user_id]
* - [name] => User name
* - [firstname] => User firstname
* - [displayname] => User displayname
* - [super] => (true|false) super admin
* - [p]
* - [permission] => true
* - ...
*
* @param string $id The blog identifier
* @param bool $with_super Includes super admins in result
*
* @return array The blog permissions.
*/
public function getBlogPermissions($id, $with_super = true)
{
$strReq = 'SELECT U.user_id AS user_id, user_super, user_name, user_firstname, ' .
'user_displayname, user_email, permissions ' .
'FROM ' . $this->prefix . 'user U ' .
'JOIN ' . $this->prefix . 'permissions P ON U.user_id = P.user_id ' .
"WHERE blog_id = '" . $this->con->escape($id) . "' ";
if ($with_super) {
$strReq .= 'UNION ' .
'SELECT U.user_id AS user_id, user_super, user_name, user_firstname, ' .
'user_displayname, user_email, NULL AS permissions ' .
'FROM ' . $this->prefix . 'user U ' .
'WHERE user_super = 1 ';
}
$rs = $this->con->select($strReq);
$res = [];
while ($rs->fetch()) {
$res[$rs->user_id] = [
'name' => $rs->user_name,
'firstname' => $rs->user_firstname,
'displayname' => $rs->user_displayname,
'email' => $rs->user_email,
'super' => (bool) $rs->user_super,
'p' => $this->auth->parsePermissions($rs->permissions),
];
}
return $res;
}
/**
* Gets the blog.
*
* @param string $id The blog identifier
*
* @return mixed The blog.
*/
public function getBlog($id)
{
$blog = $this->getBlogs(['blog_id' => $id]);
if ($blog->isEmpty()) {
return false;
}
return $blog;
}
/**
* Returns a record of blogs. <b>$params</b> is an array with the following
* optionnal parameters:
*
* - <var>blog_id</var>: Blog ID
* - <var>q</var>: Search string on blog_id, blog_name and blog_url
* - <var>limit</var>: limit results
*
* @param array|ArrayObject $params The parameters
* @param bool $count_only Count only results
*
* @return record The blogs.
*/
public function getBlogs($params = [], $count_only = false)
{
$join = ''; // %1$s
$where = ''; // %2$s
if ($count_only) {
$strReq = 'SELECT count(B.blog_id) ' .
'FROM ' . $this->prefix . 'blog B ' .
'%1$s ' .
'WHERE NULL IS NULL ' .
'%2$s ';
} else {
$strReq = 'SELECT B.blog_id, blog_uid, blog_url, blog_name, blog_desc, blog_creadt, ' .
'blog_upddt, blog_status ';
if (!empty($params['columns'])) {
$strReq .= ',';
if (is_array($params['columns'])) {
$strReq .= implode(',', $params['columns']);
} else {
$strReq .= $params['columns'];
}
$strReq .= ' ';
}
$strReq .= 'FROM ' . $this->prefix . 'blog B ' .
'%1$s ' .
'WHERE NULL IS NULL ' .
'%2$s ';
if (!empty($params['order'])) {
$strReq .= 'ORDER BY ' . $this->con->escape($params['order']) . ' ';
} else {
$strReq .= 'ORDER BY B.blog_id ASC ';
}
if (!empty($params['limit'])) {
$strReq .= $this->con->limit($params['limit']);
}
}
if ($this->auth->userID() && !$this->auth->isSuperAdmin()) {
$join = 'INNER JOIN ' . $this->prefix . 'permissions PE ON B.blog_id = PE.blog_id ';
$where = "AND PE.user_id = '" . $this->con->escape($this->auth->userID()) . "' " .
"AND (permissions LIKE '%|usage|%' OR permissions LIKE '%|admin|%' OR permissions LIKE '%|contentadmin|%') " .
'AND blog_status IN (1,0) ';
} elseif (!$this->auth->userID()) {
$where = 'AND blog_status IN (1,0) ';
}
if (isset($params['blog_status']) && $params['blog_status'] !== '' && $this->auth->isSuperAdmin()) {
$where .= 'AND blog_status = ' . (int) $params['blog_status'] . ' ';
}
if (isset($params['blog_id']) && $params['blog_id'] !== '') {
if (!is_array($params['blog_id'])) {
$params['blog_id'] = [$params['blog_id']];
}
$where .= 'AND B.blog_id ' . $this->con->in($params['blog_id']);
}
if (!empty($params['q'])) {
$params['q'] = strtolower(str_replace('*', '%', $params['q']));
$where .= 'AND (' .
"LOWER(B.blog_id) LIKE '" . $this->con->escape($params['q']) . "' " .
"OR LOWER(B.blog_name) LIKE '" . $this->con->escape($params['q']) . "' " .
"OR LOWER(B.blog_url) LIKE '" . $this->con->escape($params['q']) . "' " .
') ';
}
$strReq = sprintf($strReq, $join, $where);
return $this->con->select($strReq);
}
/**
* Adds a new blog.
*
* @param cursor $cur The blog cursor
*
* @throws Exception
*/
public function addBlog($cur)
{
if (!$this->auth->isSuperAdmin()) {
throw new Exception(__('You are not an administrator'));
}
$this->getBlogCursor($cur);
$cur->blog_creadt = date('Y-m-d H:i:s');
$cur->blog_upddt = date('Y-m-d H:i:s');
$cur->blog_uid = md5(uniqid());
$cur->insert();
}
/**
* Updates a given blog.
*
* @param string $id The blog identifier
* @param cursor $cur The cursor
*/
public function updBlog($id, $cur)
{
$this->getBlogCursor($cur);
$cur->blog_upddt = date('Y-m-d H:i:s');
$cur->update("WHERE blog_id = '" . $this->con->escape($id) . "'");
}
/**
* Gets the blog cursor.
*
* @param cursor $cur The cursor
*
* @throws Exception
*/
private function getBlogCursor($cur)
{
if (($cur->blog_id !== null
&& !preg_match('/^[A-Za-z0-9._-]{2,}$/', $cur->blog_id)) || (!$cur->blog_id)) {
throw new Exception(__('Blog ID must contain at least 2 characters using letters, numbers or symbols.'));
}
if (($cur->blog_name !== null && $cur->blog_name == '') || (!$cur->blog_name)) {
throw new Exception(__('No blog name'));
}
if (($cur->blog_url !== null && $cur->blog_url == '') || (!$cur->blog_url)) {
throw new Exception(__('No blog URL'));
}
if ($cur->blog_desc !== null) {
$cur->blog_desc = html::clean($cur->blog_desc);
}
}
/**
* Removes a given blog.
* @warning This will remove everything related to the blog (posts,
* categories, comments, links...)
*
* @param string $id The blog identifier
*
* @throws Exception
*/
public function delBlog($id)
{
if (!$this->auth->isSuperAdmin()) {
throw new Exception(__('You are not an administrator'));
}
$strReq = 'DELETE FROM ' . $this->prefix . 'blog ' .
"WHERE blog_id = '" . $this->con->escape($id) . "' ";
$this->con->execute($strReq);
}
/**
* Determines if blog exists.
*
* @param string $id The blog identifier
*
* @return bool True if blog exists, False otherwise.
*/
public function blogExists($id)
{
$strReq = 'SELECT blog_id ' .
'FROM ' . $this->prefix . 'blog ' .
"WHERE blog_id = '" . $this->con->escape($id) . "' ";
$rs = $this->con->select($strReq);
return !$rs->isEmpty();
}
/**
* Counts the number of blog posts.
*
* @param string $id The blog identifier
* @param mixed $type The post type
*
* @return integer Number of blog posts.
*/
public function countBlogPosts($id, $type = null)
{
$strReq = 'SELECT COUNT(post_id) ' .
'FROM ' . $this->prefix . 'post ' .
"WHERE blog_id = '" . $this->con->escape($id) . "' ";
if ($type) {
$strReq .= "AND post_type = '" . $this->con->escape($type) . "' ";
}
return $this->con->select($strReq)->f(0);
}
//@}
/// @name HTML Filter methods
//@{
/**
* Calls HTML filter to drop bad tags and produce valid XHTML output (if
* tidy extension is present). If <b>enable_html_filter</b> blog setting is
* false, returns not filtered string.
*
* @param string $str The string
*
* @return string
*/
public function HTMLfilter($str)
{
if ($this->blog instanceof dcBlog && !$this->blog->settings->system->enable_html_filter) {
return $str;
}
$options = new ArrayObject([
'keep_aria' => false,
'keep_data' => false,
'keep_js' => false,
]);
$this->callBehavior('HTMLfilter', $options);
$filter = new htmlFilter($options['keep_aria'], $options['keep_data'], $options['keep_js']);
$str = trim($filter->apply($str));
return $str;
}
//@}
/// @name wiki2xhtml methods
//@{
/**
* Initializes the wiki2xhtml methods.
*/
private function initWiki()
{
$this->wiki2xhtml = new wiki2xhtml();
}
/**
* Returns a transformed string with wiki2xhtml.
*
* @param string $str The string
*
* @return string
*/
public function wikiTransform($str)
{
if (!($this->wiki2xhtml instanceof wiki2xhtml)) {
$this->initWiki();
}
return $this->wiki2xhtml->transform($str);
}
/**
* Inits <var>wiki2xhtml</var> property for blog post.
*/
public function initWikiPost()
{
$this->initWiki();
$this->wiki2xhtml->setOpts([
'active_title' => 1,
'active_setext_title' => 0,
'active_hr' => 1,
'active_lists' => 1,
'active_defl' => 1,
'active_quote' => 1,
'active_pre' => 1,
'active_empty' => 1,
'active_auto_urls' => 0,
'active_auto_br' => 0,
'active_antispam' => 1,
'active_urls' => 1,
'active_auto_img' => 0,
'active_img' => 1,
'active_anchor' => 1,
'active_em' => 1,
'active_strong' => 1,
'active_br' => 1,
'active_q' => 1,
'active_code' => 1,
'active_acronym' => 1,
'active_ins' => 1,
'active_del' => 1,
'active_footnotes' => 1,
'active_wikiwords' => 0,
'active_macros' => 1,
'active_mark' => 1,
'active_aside' => 1,
'active_sup' => 1,
'active_sub' => 1,
'active_i' => 1,
'active_span' => 1,
'parse_pre' => 1,
'active_fr_syntax' => 0,
'first_title_level' => 3,
'note_prefix' => 'wiki-footnote',
'note_str' => '<div class="footnotes"><h4>Notes</h4>%s</div>',
'img_style_center' => 'display:table; margin:0 auto;',
]);
$this->wiki2xhtml->registerFunction('url:post', [$this, 'wikiPostLink']);
# --BEHAVIOR-- coreWikiPostInit
$this->callBehavior('coreInitWikiPost', $this->wiki2xhtml);
}
/**
* Inits <var>wiki2xhtml</var> property for simple blog comment (basic syntax).
*/
public function initWikiSimpleComment()
{
$this->initWiki();
$this->wiki2xhtml->setOpts([
'active_title' => 0,
'active_setext_title' => 0,
'active_hr' => 0,
'active_lists' => 0,
'active_defl' => 0,
'active_quote' => 0,
'active_pre' => 0,
'active_empty' => 0,
'active_auto_urls' => 1,
'active_auto_br' => 1,
'active_antispam' => 1,
'active_urls' => 0,
'active_auto_img' => 0,
'active_img' => 0,
'active_anchor' => 0,
'active_em' => 0,
'active_strong' => 0,
'active_br' => 0,
'active_q' => 0,
'active_code' => 0,
'active_acronym' => 0,
'active_ins' => 0,
'active_del' => 0,
'active_inline_html' => 0,
'active_footnotes' => 0,
'active_wikiwords' => 0,
'active_macros' => 0,
'active_mark' => 0,
'active_aside' => 0,
'active_sup' => 0,
'active_sub' => 0,
'active_i' => 0,
'active_span' => 0,
'parse_pre' => 0,
'active_fr_syntax' => 0,
]);
# --BEHAVIOR-- coreInitWikiSimpleComment
$this->callBehavior('coreInitWikiSimpleComment', $this->wiki2xhtml);
}
/**
* Inits <var>wiki2xhtml</var> property for blog comment.
*/
public function initWikiComment()
{
$this->initWiki();
$this->wiki2xhtml->setOpts([
'active_title' => 0,
'active_setext_title' => 0,
'active_hr' => 0,
'active_lists' => 1,
'active_defl' => 0,
'active_quote' => 1,
'active_pre' => 1,
'active_empty' => 0,
'active_auto_br' => 1,
'active_auto_urls' => 1,
'active_urls' => 1,
'active_auto_img' => 0,
'active_img' => 0,
'active_anchor' => 0,
'active_em' => 1,
'active_strong' => 1,
'active_br' => 1,
'active_q' => 1,
'active_code' => 1,
'active_acronym' => 1,
'active_ins' => 1,
'active_del' => 1,
'active_footnotes' => 0,
'active_inline_html' => 0,
'active_wikiwords' => 0,
'active_macros' => 0,
'active_mark' => 1,
'active_aside' => 0,
'active_sup' => 1,
'active_sub' => 1,
'active_i' => 1,
'active_span' => 0,
'parse_pre' => 0,
'active_fr_syntax' => 0,
]);
# --BEHAVIOR-- coreInitWikiComment
$this->callBehavior('coreInitWikiComment', $this->wiki2xhtml);
}
/**
* Get info about a post:id wiki macro
*
* @param string $url The post url
* @param string $content The content
*
* @return array
*/
public function wikiPostLink($url, $content)
{
if (!($this->blog instanceof dcBlog)) {
return [];
}
$post_id = abs((int) substr($url, 5));
if (!$post_id) {
return [];
}
$post = $this->blog->getPosts(['post_id' => $post_id]);
if ($post->isEmpty()) {
return [];
}
$res = ['url' => $post->getURL()];
$post_title = $post->post_title;
if ($content != $url) {
$res['title'] = html::escapeHTML($post->post_title);
}
if ($content == '' || $content == $url) {
$res['content'] = html::escapeHTML($post->post_title);
}
if ($post->post_lang) {
$res['lang'] = $post->post_lang;
}
return $res;
}
//@}
/// @name Maintenance methods
//@{
/**
* Creates default settings for active blog. Optionnal parameter
* <var>defaults</var> replaces default params while needed.
*
* @param array $defaults The defaults settings
*/
public function blogDefaults($defaults = null)
{
if (!is_array($defaults)) {
$defaults = [
['allow_comments', 'boolean', true,
'Allow comments on blog', ],
['allow_trackbacks', 'boolean', true,
'Allow trackbacks on blog', ],
['blog_timezone', 'string', 'Europe/London',
'Blog timezone', ],
['comments_nofollow', 'boolean', true,
'Add rel="nofollow" to comments URLs', ],
['comments_pub', 'boolean', true,
'Publish comments immediately', ],
['comments_ttl', 'integer', 0,
'Number of days to keep comments open (0 means no ttl)', ],
['copyright_notice', 'string', '', 'Copyright notice (simple text)'],
['date_format', 'string', '%A, %B %e %Y',
'Date format. See PHP strftime function for patterns', ],
['editor', 'string', '',
'Person responsible of the content', ],
['enable_html_filter', 'boolean', 0,
'Enable HTML filter', ],
['enable_xmlrpc', 'boolean', 0,
'Enable XML/RPC interface', ],
['lang', 'string', 'en',
'Default blog language', ],
['media_exclusion', 'string', '/\.(phps?|pht(ml)?|phl|.?html?|xml|js|htaccess)[0-9]*$/i',
'File name exclusion pattern in media manager. (PCRE value)', ],
['media_img_m_size', 'integer', 448,
'Image medium size in media manager', ],
['media_img_s_size', 'integer', 240,
'Image small size in media manager', ],
['media_img_t_size', 'integer', 100,
'Image thumbnail size in media manager', ],
['media_img_title_pattern', 'string', 'Title ;; Date(%b %Y) ;; separator(, )',
'Pattern to set image title when you insert it in a post', ],
['media_video_width', 'integer', 400,
'Video width in media manager', ],
['media_video_height', 'integer', 300,
'Video height in media manager', ],
['nb_post_for_home', 'integer', 20,
'Number of entries on first home page', ],
['nb_post_per_page', 'integer', 20,
'Number of entries on home pages and category pages', ],
['nb_post_per_feed', 'integer', 20,
'Number of entries on feeds', ],
['nb_comment_per_feed', 'integer', 20,
'Number of comments on feeds', ],
['post_url_format', 'string', '{y}/{m}/{d}/{t}',
'Post URL format. {y}: year, {m}: month, {d}: day, {id}: post id, {t}: entry title', ],
['public_path', 'string', 'public',
'Path to public directory, begins with a / for a full system path', ],
['public_url', 'string', '/public',
'URL to public directory', ],
['robots_policy', 'string', 'INDEX,FOLLOW',
'Search engines robots policy', ],
['short_feed_items', 'boolean', false,
'Display short feed items', ],
['theme', 'string', 'berlin',
'Blog theme', ],
['themes_path', 'string', 'themes',
'Themes root path', ],
['themes_url', 'string', '/themes',
'Themes root URL', ],
['time_format', 'string', '%H:%M',
'Time format. See PHP strftime function for patterns', ],
['tpl_allow_php', 'boolean', false,
'Allow PHP code in templates', ],
['tpl_use_cache', 'boolean', true,
'Use template caching', ],
['trackbacks_pub', 'boolean', true,
'Publish trackbacks immediately', ],
['trackbacks_ttl', 'integer', 0,
'Number of days to keep trackbacks open (0 means no ttl)', ],
['url_scan', 'string', 'query_string',
'URL handle mode (path_info or query_string)', ],
['use_smilies', 'boolean', false,
'Show smilies on entries and comments', ],
['no_search', 'boolean', false,
'Disable search', ],
['inc_subcats', 'boolean', false,
'Include sub-categories in category page and category posts feed', ],
['wiki_comments', 'boolean', false,
'Allow commenters to use a subset of wiki syntax', ],
['import_feed_url_control', 'boolean', true,
'Control feed URL before import', ],
['import_feed_no_private_ip', 'boolean', true,
'Prevent import feed from private IP', ],
['import_feed_ip_regexp', 'string', '',
'Authorize import feed only from this IP regexp', ],
['import_feed_port_regexp', 'string', '/^(80|443)$/',
'Authorize import feed only from this port regexp', ],
['jquery_needed', 'boolean', true,
'Load jQuery library', ],
];
}
$settings = new dcSettings($this, null);
$settings->addNamespace('system');
foreach ($defaults as $v) {
$settings->system->put($v[0], $v[2], $v[1], $v[3], false, true);
}
}
/**
* Recreates entries search engine index.
*
* @param mixed $start The start entry index
* @param mixed $limit The limit of entry to index
*
* @return mixed sum of <var>$start</var> and <var>$limit</var>
*/
public function indexAllPosts($start = null, $limit = null)
{
$strReq = 'SELECT COUNT(post_id) ' .
'FROM ' . $this->prefix . 'post';
$rs = $this->con->select($strReq);
$count = $rs->f(0);
$strReq = 'SELECT post_id, post_title, post_excerpt_xhtml, post_content_xhtml ' .
'FROM ' . $this->prefix . 'post ';
if ($start !== null && $limit !== null) {
$strReq .= $this->con->limit($start, $limit);
}
$rs = $this->con->select($strReq, true);
$cur = $this->con->openCursor($this->prefix . 'post');
while ($rs->fetch()) {
$words = $rs->post_title . ' ' . $rs->post_excerpt_xhtml . ' ' .
$rs->post_content_xhtml;
$cur->post_words = implode(' ', text::splitWords($words));
$cur->update('WHERE post_id = ' . (int) $rs->post_id);
$cur->clean();
}
if ($start + $limit > $count) {
return;
}
return $start + $limit;
}
/**
* Recreates comments search engine index.
*
* @param mixed $start The start comment index
* @param mixed $limit The limit of comment to index
*
* @return mixed sum of <var>$start</var> and <var>$limit</var>
*/
public function indexAllComments($start = null, $limit = null)
{
$strReq = 'SELECT COUNT(comment_id) ' .
'FROM ' . $this->prefix . 'comment';
$rs = $this->con->select($strReq);
$count = $rs->f(0);
$strReq = 'SELECT comment_id, comment_content ' .
'FROM ' . $this->prefix . 'comment ';
if ($start !== null && $limit !== null) {
$strReq .= $this->con->limit($start, $limit);
}
$rs = $this->con->select($strReq);
$cur = $this->con->openCursor($this->prefix . 'comment');
while ($rs->fetch()) {
$cur->comment_words = implode(' ', text::splitWords($rs->comment_content));
$cur->update('WHERE comment_id = ' . (int) $rs->comment_id);
$cur->clean();
}
if ($start + $limit > $count) {
return;
}
return $start + $limit;
}
/**
* Reinits nb_comment and nb_trackback in post table.
*/
public function countAllComments()
{
$updCommentReq = 'UPDATE ' . $this->prefix . 'post P ' .
'SET nb_comment = (' .
'SELECT COUNT(C.comment_id) from ' . $this->prefix . 'comment C ' .
'WHERE C.post_id = P.post_id AND C.comment_trackback <> 1 ' .
'AND C.comment_status = 1 ' .
')';
$updTrackbackReq = 'UPDATE ' . $this->prefix . 'post P ' .
'SET nb_trackback = (' .
'SELECT COUNT(C.comment_id) from ' . $this->prefix . 'comment C ' .
'WHERE C.post_id = P.post_id AND C.comment_trackback = 1 ' .
'AND C.comment_status = 1 ' .
')';
$this->con->execute($updCommentReq);
$this->con->execute($updTrackbackReq);
}
/**
* Empty templates cache directory
*/
public function emptyTemplatesCache()
{
if (is_dir(DC_TPL_CACHE . '/cbtpl')) {
files::deltree(DC_TPL_CACHE . '/cbtpl');
}
}
/**
* Return elapsed time since script has been started
*
* @param mixed $mtime timestamp (microtime format) to evaluate
* delta from current time is taken if null
*
* @return mixed The elapsed time.
*/
public function getElapsedTime($mtime = null)
{
if ($mtime !== null) {
return $mtime - $this->stime;
}
return microtime(true) - $this->stime;
}
//@}
}