<?php
/**
* @brief Dotclear metadata class.
*
* Dotclear metadata class instance is provided by dcCore $meta property.
*
* @package Dotclear
* @subpackage Core
*
* @copyright Olivier Meunier & Association Dotclear
* @copyright GPL-2.0-only
*/
if (!defined('DC_RC_PATH')) {
return;
}
class dcMeta
{
private $core; ///< <b>dcCore</b> dcCore instance
private $con; ///< <b>connection</b> Database connection object
private $table; ///< <b>string</b> Media table name
/**
* Constructs a new instance.
*
* @param dcCore $core The core
*/
public function __construct(dcCore $core)
{
$this->core = &$core;
$this->con = &$this->core->con;
$this->table = $this->core->prefix . 'meta';
}
/**
* Splits up comma-separated values into an array of
* unique, URL-proof metadata values.
*
* @param string $str Comma-separated metadata
*
* @return array The array of sanitized metadata
*/
public function splitMetaValues($str)
{
$res = [];
foreach (explode(',', $str) as $i => $tag) {
$tag = trim((string) $tag);
$tag = self::sanitizeMetaID($tag);
if ($tag != false) {
$res[$i] = $tag;
}
}
return array_unique($res);
}
/**
* Make a metadata ID URL-proof.
*
* @param string $str The metadata ID
*
* @return string
*/
public static function sanitizeMetaID($str)
{
return text::tidyURL($str, false, true);
}
/**
* Converts serialized metadata (for instance in dc_post post_meta)
* into a meta array.
*
* @param string $str The serialized metadata
*
* @return array The meta array.
*/
public function getMetaArray($str)
{
$meta = @unserialize((string) $str);
if (!is_array($meta)) {
return [];
}
return $meta;
}
/**
* Converts serialized metadata (for instance in dc_post post_meta)
* into a comma-separated meta list for a given type.
*
* @param string $str The serialized metadata
* @param string $type The meta type to retrieve metaIDs from
*
* @return string The comma-separated list of meta.
*/
public function getMetaStr($str, $type)
{
$meta = $this->getMetaArray($str);
if (!isset($meta[$type])) {
return '';
}
return implode(', ', $meta[$type]);
}
/**
* Converts serialized metadata (for instance in dc_post post_meta)
* into a "fetchable" metadata record.
*
* @param string $str The serialized metadata
* @param string $type The meta type to retrieve metaIDs from
*
* @return staticRecord The meta recordset.
*/
public function getMetaRecordset($str, $type)
{
$meta = $this->getMetaArray($str);
$data = [];
if (isset($meta[$type])) {
foreach ($meta[$type] as $v) {
$data[] = [
'meta_id' => $v,
'meta_type' => $type,
'meta_id_lower' => mb_strtolower($v),
'count' => 0,
'percent' => 0,
'roundpercent' => 0,
];
}
}
return staticRecord::newFromArray($data);
}
/**
* Checks whether the current user is allowed to change post meta
* An exception is thrown if user is not allowed.
*
* @param mixed $post_id The post identifier
*
* @throws Exception
*/
private function checkPermissionsOnPost($post_id)
{
$post_id = (int) $post_id;
if (!$this->core->auth->check('usage,contentadmin', $this->core->blog->id)) {
throw new Exception(__('You are not allowed to change this entry status'));
}
# If user can only publish, we need to check the post's owner
if (!$this->core->auth->check('contentadmin', $this->core->blog->id)) {
$sql = new dcSelectStatement($this->core, 'dcMetaCheckPermissionsOnPost');
$sql
->from($sql->core->prefix . 'post')
->column('post_id')
->where('post_id = ' . $post_id)
->and('user_id = ' . $sql->quote($this->core->auth->userID(), true));
$rs = $sql->select();
if ($rs->isEmpty()) {
throw new Exception(__('You are not allowed to change this entry status'));
}
}
}
/**
* Updates serialized post_meta information with dc_meta table information.
*
* @param mixed $post_id The post identifier
*/
private function updatePostMeta($post_id)
{
$post_id = (int) $post_id;
$sql = new dcSelectStatement($this->core, 'dcMetaUpdatePostMeta');
$sql
->from($this->table)
->columns([
'meta_id',
'meta_type',
])
->where('post_id = ' . $post_id);
$rs = $sql->select();
$meta = [];
while ($rs->fetch()) {
$meta[$rs->meta_type][] = $rs->meta_id;
}
$post_meta = serialize($meta);
$cur = $this->con->openCursor($this->core->prefix . 'post');
$cur->post_meta = $post_meta;
$sql = new dcUpdateStatement($this->core, 'dcMetaUpdatePostMeta');
$sql->where('post_id = ' . $post_id);
$sql->update($cur);
$this->core->blog->triggerBlog();
}
/**
* Retrieves posts corresponding to given meta criteria.
* <b>$params</b> is an array taking the following optional parameters:
* - meta_id : get posts having meta id
* - meta_type : get posts having meta type
*
* @param array $params The parameters
* @param bool $count_only Only count results
* @param dcSelectStatement|null $sql Optional dcSqlStatement instance
*
* @return mixed The resulting posts record.
*/
public function getPostsByMeta($params = [], $count_only = false, ?dcSelectStatement $sql = null)
{
if (!isset($params['meta_id'])) {
return;
}
if (!$sql) {
$sql = new dcSelectStatement($this->core, 'dcMetaGetPostsByMeta');
}
$sql
->from($this->table . ' META')
->and('META.post_id = P.post_id')
->and('META.meta_id = ' . $sql->quote($params['meta_id'], true));
if (!empty($params['meta_type'])) {
$sql->and('META.meta_type = ' . $sql->quote($params['meta_type'], true));
unset($params['meta_type']);
}
unset($params['meta_id']);
return $this->core->blog->getPosts($params, $count_only, $sql);
}
/**
* Retrieves comments corresponding to given meta criteria.
* <b>$params</b> is an array taking the following optional parameters:
* - meta_id : get posts having meta id
* - meta_type : get posts having meta type
*
* @param array $params The parameters
* @param bool $count_only Only count results
* @param dcSelectStatement|null $sql Optional dcSqlStatement instance
*
* @return mixed The resulting comments record.
*/
public function getCommentsByMeta($params = [], $count_only = false, ?dcSelectStatement $sql = null)
{
if (!isset($params['meta_id'])) {
return;
}
if (!$sql) {
$sql = new dcSelectStatement($this->core, 'dcMetaGetPostsByMeta');
}
$sql
->from($this->table . ' META')
->and('META.post_id = P.post_id')
->and('META.meta_id = ' . $sql->quote($params['meta_id'], true));
if (!empty($params['meta_type'])) {
$sql->and('META.meta_type = ' . $sql->quote($params['meta_type'], true));
unset($params['meta_type']);
}
return $this->core->blog->getComments($params, $count_only, $sql);
}
/**
* Generic-purpose metadata retrieval : gets metadatas according to given
* criteria. <b>$params</b> is an array taking the following
* optionnal parameters:
*
* - type: get metas having the given type
* - meta_id: if not null, get metas having the given id
* - post_id: get metas for the given post id
* - limit: number of max fetched metas
* - order: results order (default : posts count DESC)
*
* @param array $params The parameters
* @param bool $count_only Only counts results
* @param dcSelectStatement|null $sql Optional dcSqlStatement instance
*
* @return record The metadata.
*/
public function getMetadata($params = [], $count_only = false, ?dcSelectStatement $sql = null)
{
if (!$sql) {
$sql = new dcSelectStatement($this->core, 'dcMetaGetMetadata');
}
if ($count_only) {
$sql->column($sql->count($sql->unique('M.meta_id')));
} else {
$sql->columns([
'M.meta_id',
'M.meta_type',
$sql->count('M.post_id', 'count'),
$sql->max('P.post_dt', 'latest'),
$sql->min('P.post_dt', 'oldest'),
]);
}
$sql
->from($this->table . ' M')
->join(
(new dcJoinStatement($this->core, 'dcMetaGetMetadata'))
->type('LEFT')
->from($sql->core->prefix . 'post P')
->on('M.post_id = P.post_id')
->statement()
)
->where('P.blog_id = ' . $sql->quote($this->core->blog->id, true));
if (isset($params['meta_type'])) {
$sql->and('meta_type = ' . $sql->quote($params['meta_type'], true));
}
if (isset($params['meta_id'])) {
$sql->and('meta_id = ' . $sql->quote($params['meta_id'], true));
}
if (isset($params['post_id'])) {
$sql->and('P.post_id' . $sql->in($params['post_id']));
}
if (!$this->core->auth->check('contentadmin', $this->core->blog->id)) {
$user_id = $this->core->auth->userID();
$and = ['post_status = 1'];
if ($this->core->blog->without_password) {
$and[] = 'post_password IS NULL';
}
$or = [$sql->andGroup($and)];
if ($user_id) {
$or[] = 'P.user_id = ' . $sql->quote($user_id, true);
}
$sql->and($sql->orGroup($or));
}
if (!$count_only) {
if (!isset($params['order'])) {
$params['order'] = 'count DESC';
}
$sql
->group([
'meta_id',
'meta_type',
'P.blog_id',
])
->order($params['order']);
if (isset($params['limit'])) {
$sql->limit($params['limit']);
}
}
$rs = $sql->select();
return $rs;
}
/**
* Computes statistics from a metadata recordset.
* Each record gets enriched with lowercase name, percent and roundpercent columns
*
* @param record $rs The metadata recordset
*
* @return staticRecord The meta statistics.
*/
public function computeMetaStats($rs)
{
$rs_static = $rs->toStatic();
$max = [];
while ($rs_static->fetch()) {
$type = $rs_static->meta_type;
if (!isset($max[$type])) {
$max[$type] = $rs_static->count;
} else {
if ($rs_static->count > $max[$type]) {
$max[$type] = $rs_static->count;
}
}
}
$rs_static->moveStart();
while ($rs_static->fetch()) { // @phpstan-ignore-line
$rs_static->set('meta_id_lower', dcUtils::removeDiacritics(mb_strtolower($rs_static->meta_id)));
$percent = ((int) $rs_static->count) * 100 / $max[$rs_static->meta_type];
$rs_static->set('percent', (int) round($percent));
$rs_static->set('roundpercent', round($percent / 10) * 10);
}
return $rs_static;
}
/**
* Adds a metadata to a post.
*
* @param mixed $post_id The post identifier
* @param mixed $type The type
* @param mixed $value The value
*/
public function setPostMeta($post_id, $type, $value)
{
$this->checkPermissionsOnPost($post_id);
$value = trim((string) $value);
if ($value === '') {
return;
}
$cur = $this->con->openCursor($this->table);
$cur->post_id = (int) $post_id;
$cur->meta_id = (string) $value;
$cur->meta_type = (string) $type;
$cur->insert();
$this->updatePostMeta((int) $post_id);
}
/**
* Removes metadata from a post.
*
* @param mixed $post_id The post identifier
* @param mixed $type The meta type (if null, delete all types)
* @param mixed $meta_id The meta identifier (if null, delete all values)
*/
public function delPostMeta($post_id, $type = null, $meta_id = null)
{
$post_id = (int) $post_id;
$this->checkPermissionsOnPost($post_id);
$sql = new dcDeleteStatement($this->core, 'dcMetaDelPostMeta');
$sql
->from($this->table)
->where('post_id = ' . $post_id);
if ($type !== null) {
$sql->and('meta_type = ' . $sql->quote($type, true));
}
if ($meta_id !== null) {
$sql->and('meta_id = ' . $sql->quote($meta_id, true));
}
$sql->delete();
$this->updatePostMeta((int) $post_id);
}
/**
* Mass updates metadata for a given post_type.
*
* @param string $meta_id The old meta value
* @param string $new_meta_id The new meta value
* @param mixed $type The type (if null, select all types)
* @param mixed $post_type The post type (if null, select all types)
*
* @return bool true if at least 1 post has been impacted
*/
public function updateMeta($meta_id, $new_meta_id, $type = null, $post_type = null)
{
$new_meta_id = self::sanitizeMetaID($new_meta_id);
if ($new_meta_id == $meta_id) {
return true;
}
$sql = new dcSelectStatement($this->core, 'dcMetaUpdateMeta');
$sql
->from([
$this->table . ' M',
$sql->core->prefix . 'post P',
])
->column('M.post_id')
->where('P.post_id = M.post_id')
->and('P.blog_id = ' . $sql->quote($this->core->blog->id, true));
if (!$this->core->auth->check('contentadmin', $this->core->blog->id)) {
$sql->and('P.user_id = ' . $sql->quote($this->core->auth->userID(), true));
}
if ($post_type !== null) {
$sql->and('P.post_type = ' . $sql->quote($post_type, true));
}
if ($type !== null) {
$sql->and('meta_type = ' . $sql->quote($type, true));
}
$to_update = $to_remove = [];
// Clone $sql object in order to do the same select query but with another meta_id
$sqlNew = clone $sql;
$sql->and('meta_id = ' . $sql->quote($meta_id, true));
$rs = $sql->select();
while ($rs->fetch()) {
$to_update[] = $rs->post_id;
}
if (empty($to_update)) {
return false;
}
$sqlNew->and('meta_id = ' . $sqlNew->quote($new_meta_id, true));
$rs = $sqlNew->select();
while ($rs->fetch()) {
if (in_array($rs->post_id, $to_update)) {
$to_remove[] = $rs->post_id;
unset($to_update[array_search($rs->post_id, $to_update)]);
}
}
# Delete duplicate meta
if (!empty($to_remove)) {
$sqlDel = new dcDeleteStatement($this->core, 'dcMetaUpdateMeta');
$sqlDel
->from($this->table)
->where('post_id' . $sqlDel->in($to_remove, 'int')) // Note: will cast all values to integer
->and('meta_id = ' . $sqlDel->quote($meta_id, true));
if ($type !== null) {
$sqlDel->and('meta_type = ' . $sqlDel->quote($type, true));
}
$sqlDel->delete();
foreach ($to_remove as $post_id) {
$this->updatePostMeta($post_id);
}
}
# Update meta
if (!empty($to_update)) {
$sqlUpd = new dcUpdateStatement($this->core, 'dcMetaUpdateMeta');
$sqlUpd
->from($this->table)
->set('meta_id = ' . $sqlUpd->quote($new_meta_id, true))
->where('post_id' . $sqlUpd->in($to_update, 'int')) // Note: will cast all values to integer
->and('meta_id = ' . $sqlUpd->quote($meta_id, true));
if ($type !== null) {
$sqlUpd->and('meta_type = ' . $sqlUpd->quote($type, true));
}
$sqlUpd->update();
foreach ($to_update as $post_id) {
$this->updatePostMeta($post_id);
}
}
return true;
}
/**
* Mass delete metadata for a given post_type.
*
* @param string $meta_id The meta identifier
* @param mixed $type The meta type (if null, select all types)
* @param mixed $post_type The post type (if null, select all types)
*
* @return array The list of impacted post_ids
*/
public function delMeta($meta_id, $type = null, $post_type = null)
{
$sql = new dcSelectStatement($this->core, 'dcMetaDelMeta');
$sql
->column('M.post_id')
->from([
$this->table . ' M',
$sql->core->prefix . 'post P',
])
->where('P.post_id = M.post_id')
->and('P.blog_id = ' . $sql->quote($this->core->blog->id, true))
->and('meta_id = ' . $sql->quote($meta_id, true));
if ($type !== null) {
$sql->and('meta_type = ' . $sql->quote($type, true));
}
if ($post_type !== null) {
$sql->and('P.post_type = ' . $sql->quote($post_type, true));
}
$rs = $sql->select();
if ($rs->isEmpty()) {
return [];
}
$ids = [];
while ($rs->fetch()) {
$ids[] = $rs->post_id;
}
$sql = new dcDeleteStatement($this->core, 'dcMetaDelMeta');
$sql
->from($this->table)
->where('post_id' . $sql->in($ids, 'int'))
->and('meta_id = ' . $sql->quote($meta_id, true));
if ($type !== null) {
$sql->and('meta_type = ' . $sql->quote($type, true));
}
$sql->delete();
foreach ($ids as $post_id) {
$this->updatePostMeta($post_id);
}
return $ids;
}
}