Sequenzia/app/models/Post/TagMethods.php

394 lines
14 KiB
PHP
Executable File

<?php
trait PostTagMethods
{
public $new_tags;
public $old_tags;
public $old_cached_tags;
protected $tags_list = [];
private $tags_string;
static public function find_by_tags($tags, array $options = array())
{
list ($sql, $params) = Post::generate_sql($tags, $options);
return Post::findBySql($sql, $params);
}
static public function recalculate_cached_tags($id = null)
{
// conds = array()
// cond_params = array()
// sql = %{
// UPDATE posts p SET cached_tags = (
// SELECT array_to_string(coalesce(array(
// SELECT t.name
// FROM tags t, posts_tags pt
// WHERE t.id = pt.tag_id AND pt.post_id = p.id
// ORDER BY t.name
// ), 'array()'::textarray()), ' ')
// )
// }
// if ((id) {) {
// conds << "WHERE p.id = ?"
// cond_params << id
// }
// sql = [sql, conds].join(" ")
// executeSql sql, *cond_params
}
# new, previous and latest are History objects for cached_tags. Split
# the tag changes apart.
static public function tag_changes($new, $previous, $latest)
{
$new_tags = explode(' ', $new->value);
if ($previous && $previous->value)
$old_tags = explode(' ', $previous->value);
else
$old_tags = [];
$latest_tags = explode(' ', $latest->value);
return [
'added_tags' => array_diff($new_tags, $old_tags),
'removed_tags' => array_diff($old_tags, $new_tags),
'unchanged_tags' => array_intersect($new_tags, $old_tags),
'obsolete_added_tags' => array_diff(array_diff($new_tags, $old_tags), $latest_tags),
'obsolete_removed_tags' => array_intersect(array_diff($old_tags, $new_tags), $latest_tags),
];
}
public function cached_tags_undo($change, $redo_changes = false)
{
$current_tags = explode(' ', $this->cached_tags);
$prev = $change->previous;
if ($redo_changes) {
list($change, $prev) = [$prev, $change];
}
$changes = self::tag_changes($change, $prev, $change->latest());
$new_tags = array_unique(array_merge(array_diff($current_tags, $changes['added_tags']), $changes['removed_tags']));
$this->setTags(implode(' ', $new_tags));
}
public function cached_tags_redo($change)
{
$this->cached_tags_undo($change, true);
}
# === Parameters
# * :tag<String>:: the tag to search for
public function has_tag($tag)
{
return array_search($tag, $this->tags) !== false;
}
# Returns the tags in a URL suitable string
public function tag_title()
{
return substr(preg_replace('/\W+/', '-', $this->cached_tags), 0, 50);
}
# Return the tags we display in URLs, page titles, etc.
public function title_tags()
{
return $this->cached_tags;
}
public function tags()
{
return $this->getTags();
}
public function getTags()
{
return explode(' ', $this->cached_tags);
}
# Sets the tags for the post. Does not actually save anything to the database when called.
#
# === Parameters
# * :tags<String>:: a whitespace delimited list of tags
public function setTags($tags)
{
$this->new_tags = array_filter(Tag::scan_tags($tags));
$current_tags = explode(' ', $this->cached_tags);
if ($this->new_tags != $current_tags)
$this->touch_change_seq();
$this->tags_list = $tags;
}
# Returns all versioned tags and metatags.
public function cached_tags_versioned()
{
return "rating:" . $this->rating . ' ' . $this->cached_tags;
}
# Commit metatags; this is done before save, so any changes are stored normally.
/**
* Moved commit_metatags to be executed before commit_tags to avoid re-saving the
* model. This might have Unforeseen Consequences, but nothing detected so far.
*/
protected function commit_metatags()
{
foreach ($this->new_tags as $k => $tag) {
# Moved this one outside the switch because preg_match
# doesn't get along well with switch.
if (preg_match('/^source:(.*)/', $tag, $m)) {
unset($this->new_tags[$k]);
$this->source = $m[1];
continue;
}
switch ($tag) {
case 'hold':
$this->is_held = true;
unset($this->new_tags[$k]);
break;
case 'unhold':
$this->is_held = false;
unset($this->new_tags[$k]);
break;
case 'show':
$this->is_shown_in_index = true;
unset($this->new_tags[$k]);
break;
case 'hide':
$this->is_shown_in_index = false;
unset($this->new_tags[$k]);
break;
case '+flag':
unset($this->new_tags[$k]);
# Permissions for this are checked on commit.
$this->metatag_flagged = "moderator flagged";
break;
}
}
}
# Commit any tag changes to the database. This is done after save, so any changes
# must be made directly to the database.
/**
* It's strange how actions like changing pool's sequence, that updates the post via
* update_batch and sends only one metatag: 'pool:$id:$seq' and doesn't send old_tags, don't
* mess up the rest of the tags (because that metatag is discarted, and the post is left without
* old_tags or any tag).
* So we're gonna do a workaround for that: if the new tags only contain metatags, we won't
* take any further action but commit such metatags.
* We'll use $had_metatags for that.
*/
protected function commit_tags()
{
if ($this->isNewRecord() || !$this->new_tags)
return;
if ($this->old_tags) {
# If someone else committed changes to this post before we did,
# then try to merge the tag changes together.
$current_tags = explode(' ', $this->cached_tags);
$this->old_tags = Tag::scan_tags($this->old_tags);
$this->new_tags = array_filter(array_merge(array_diff(array_merge($current_tags, $this->new_tags), $this->old_tags), array_intersect($current_tags, $this->new_tags)));
}
$this->commit_metatags();
$meta_tags = ['-pool:', 'pool:', 'rating:', 'parent:', 'child:', 'source:'];
$ratings = ['q', 's', 'e'];
$had_metatags = false;
foreach ($this->new_tags as $k => $tag) {
# To avoid preg_match.
$is_mt = false;
foreach ($meta_tags as $mt) {
if (strpos($tag, $mt) === 0 || in_array($tag, $ratings)) {
$is_mt = true;
break;
}
}
if (!$is_mt)
continue;
$had_metatags = true;
unset($this->new_tags[$k]);
if (in_array($tag, $ratings))
$tag = 'rating:'.$tag;
$subparam = explode(':', $tag, 3);
$metatag = array_shift($subparam);
$param = array_shift($subparam);
$subparam = empty($subparam) ? null : array_shift($subparam);
switch($metatag) {
case 'rating':
# Change rating. This will override rating selected on radio buttons.
if (in_array($param, $ratings))
$this->rating = $param;
break;
case 'pool':
try {
$name = $param;
$seq = $subparam;
# Workaround: I don't understand how can the pool be found when finding_by_name
# using the id.
if (ctype_digit($name))
$pool = Pool::where(['id' => $name])->first();
else
$pool = Pool::where(['name' => $name])->first();
# Set :ignore_already_exists, so pool:1:2 can be used to change the sequence number
# of a post that already exists in the pool.
$options = array('user' => User::where('id = ?', $this->updater_user_id)->first(), 'ignore_already_exists' => true);
if ($seq)
$options['sequence'] = $seq;
if (!$pool and !ctype_digit($name))
$pool = Pool::create(array('name' => $name, 'is_public' => false, 'user_id' => $this->updater_user_id));
if (!$pool || !$pool->can_change(current_user(), null))
continue 2;
$pool->add_post($this->id, $options);
} catch(Pool_PostAlreadyExistsError $e) {
} catch (Pool_AccessDeniedError $e) {
}
break;
case '-pool':
$name = $param;
$cmd = $subparam;
$pool = Pool::where(['name' => $name])->first();
if (!$pool->can_change(current_user(), null))
break;
if ($cmd == "parent") {
# If we have a parent, remove ourself from the pool and add our parent in
# our place. If we have no parent, do nothing and leave us in the pool.
if (!empty($this->parent_id)) {
$pool->transfer_post_to_parent($this->id, $this->parent_id);
break;
}
}
$pool && $pool->remove_post($id);
break;
case 'source':
$this->source = $param;
break;
case 'parent':
if (is_numeric($param)) {
$this->parent_id = (int)$param;;
}
break;
case 'child':
unset($this->new_tags[$k]);
break;
}
}
if (!$this->new_tags) {
if ($had_metatags)
return;
$this->new_tags[] = "tagme";
}
// $this->tags = implode(' ', array_unique(TagImplication::with_implied(TagAlias::to_aliased($this->new_tags))));
$this->new_tags = TagAlias::to_aliased($this->new_tags);
$this->new_tags = array_unique(TagImplication::with_implied($this->new_tags));
sort($this->new_tags);
// $this->tags = implode(' ', $this->tags());
# TODO: be more selective in deleting from the join table
self::connection()->executeSql("DELETE FROM posts_tags WHERE post_id = ?", $this->id);
$new_tags_ids = [];
$new_tags_names = [];
$this->new_tags = array_map(function ($x) use (&$new_tags_ids, &$new_tags_names) {
$tag = Tag::find_or_create_by_name($x);
if (!in_array($tag->id, $new_tags_ids)) {
$new_tags_ids[] = $tag->id;
$new_tags_names[] = $tag->name;
return $tag;
}
}, $this->new_tags);
unset($new_tags_ids);
$this->new_tags = array_filter($this->new_tags);
# If any tags are newly active, expire the tag cache.
if ($this->new_tags) {
$any_new_tags = false;
$previous_tags = explode(' ', $this->cached_tags);
foreach ($this->new_tags as $tag) {
# If this tag is in old_tags, then it's already active and we just removed it
# in the above DELETE, so it's not really a newly activated tag. (This isn't
# self.old_tags; that's the tags the user saw before he edited, not the data
# we're replacing.)
if ($tag->post_count == 0 and !in_array($tag->name, $previous_tags)) {
$any_new_tags = true;
break;
}
}
if ($any_new_tags) {
Moebooru\CacheHelper::expire_tag_version();
}
}
# Sort
sort($new_tags_names);
sort($this->new_tags);
$tag_set = implode(", ", array_map(function($x){return "(".$this->id.", ".$x->id.")";}, $this->new_tags));
$this->cached_tags = implode(' ', $new_tags_names);
$sql = "INSERT INTO posts_tags (post_id, tag_id) VALUES " . $tag_set;
self::connection()->executeSql($sql);
# Store the old cached_tags, so we can expire them.
$this->old_cached_tags = $this->cached_tags;
// Commenting the following line; it will cause cached_tags to not to be updated.
// They're set above.
// $this->cached_tags = self::connection()->selectValue("SELECT cached_tags FROM posts WHERE id = " . $this->id);
$this->new_tags = null;
}
protected function save_post_history()
{
$new_cached_tags = $this->cached_tags_versioned();
if ($this->tag_history->none() or $this->tag_history[0]->tags != $new_cached_tags) {
PostTagHistory::create([
'post_id' => $this->id,
'tags' => $new_cached_tags,
'user_id' => $this->user_id,
'ip_addr' => current_user() && current_user()->ip_addr ? current_user()->ip_addr : "127.0.0.1"
]);
}
}
public function tag_names()
{
return $this->cached_tags;
}
}