Some work on ActivityModeration with notice deletion

Let's now create an event called DeleteNotice and also make sure we
handle the onNoticeDeleteRelated properly in ActivityModeration to
avoid possible endless loops etc.
This commit is contained in:
Mikael Nordfeldth 2015-10-03 02:02:37 +02:00
parent ae73baf4ee
commit 88f7bb1ed5
8 changed files with 346 additions and 80 deletions

View File

@ -1,54 +0,0 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
* Copyright (C) 2008, 2009, Control Yourself, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('GNUSOCIAL')) { exit(1); }
/**
* Table Definition for deleted_notice
*/
class Deleted_notice extends Managed_DataObject
{
public $__table = 'deleted_notice'; // table name
public $id; // int(4) primary_key not_null
public $profile_id; // int(4) not_null
public $uri; // varchar(191) unique_key not 255 because utf8mb4 takes more space
public $created; // datetime() not_null
public $deleted; // datetime() not_null
public static function schemaDef()
{
return array(
'fields' => array(
'id' => array('type' => 'int', 'not null' => true, 'description' => 'identity of notice'),
'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'author of the notice'),
'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'),
'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the notice record was created'),
'deleted' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the notice record was created'),
),
'primary key' => array('id'),
'unique keys' => array(
'deleted_notice_uri_key' => array('uri'),
),
'indexes' => array(
'deleted_notice_profile_id_idx' => array('profile_id'),
),
);
}
}

View File

@ -163,34 +163,11 @@ class Notice extends Managed_DataObject
if ($this->getProfile()->sameAs($actor) || $actor->hasRight(Right::DELETEOTHERSNOTICE)) { if ($this->getProfile()->sameAs($actor) || $actor->hasRight(Right::DELETEOTHERSNOTICE)) {
return $this->delete(); return $this->delete();
} }
throw new AuthorizationException('You are not allowed to delete other user\'s notices'); throw new AuthorizationException(_('You are not allowed to delete other user\'s notices'));
} }
function delete($useWhere=false) public function delete($useWhere=false)
{ {
// For auditing purposes, save a record that the notice
// was deleted.
// @fixme we have some cases where things get re-run and so the
// insert fails.
$deleted = Deleted_notice::getKV('id', $this->id);
if (!$deleted instanceof Deleted_notice) {
$deleted = Deleted_notice::getKV('uri', $this->uri);
}
if (!$deleted instanceof Deleted_notice) {
$deleted = new Deleted_notice();
$deleted->id = $this->id;
$deleted->profile_id = $this->profile_id;
$deleted->uri = $this->uri;
$deleted->created = $this->created;
$deleted->deleted = common_sql_now();
$deleted->insert();
}
if (Event::handle('NoticeDeleteRelated', array($this))) { if (Event::handle('NoticeDeleteRelated', array($this))) {
// Clear related records // Clear related records

View File

@ -72,7 +72,6 @@ $classes = array('Schema_version',
'Group_block', 'Group_block',
'Group_alias', 'Group_alias',
'Session', 'Session',
'Deleted_notice',
'Config', 'Config',
'Profile_role', 'Profile_role',
'Location_namespace', 'Location_namespace',

View File

@ -306,6 +306,7 @@ $default =
array('core' => array( array('core' => array(
'ActivityVerb' => array(), 'ActivityVerb' => array(),
'ActivityVerbPost' => array(), 'ActivityVerbPost' => array(),
'ActivityModeration' => array(),
'AuthCrypt' => array(), 'AuthCrypt' => array(),
'Cronish' => array(), 'Cronish' => array(),
'Favorite' => array(), 'Favorite' => array(),

View File

@ -0,0 +1,118 @@
<?php
/**
* @package Activity
* @maintainer Mikael Nordfeldth <mmn@hethane.se>
*/
class ActivityModerationPlugin extends ActivityVerbHandlerPlugin
{
public function tag()
{
return 'actmod';
}
public function types()
{
return array();
}
public function verbs()
{
return array(ActivityVerb::DELETE);
}
public function onBeforePluginCheckSchema()
{
Deleted_notice::beforeSchemaUpdate();
return true;
}
public function onCheckSchema()
{
$schema = Schema::get();
$schema->ensureTable('deleted_notice', Deleted_notice::schemaDef());
return true;
}
protected function getActionTitle(ManagedAction $action, $verb, Notice $target, Profile $scoped)
{
// FIXME: switch based on action type
return _m('TITLE', 'Notice moderation');
}
protected function doActionPreparation(ManagedAction $action, $verb, Notice $target, Profile $scoped)
{
// pass
}
protected function doActionPost(ManagedAction $action, $verb, Notice $target, Profile $scoped)
{
switch (true) {
case ActivityUtils::compareVerbs($verb, array(ActivityVerb::DELETE)):
// do whatever preparation is necessary to delete a verb
$target->delete();
break;
default:
throw new ServerException('ActivityVerb POST not handled by plugin that was supposed to do it.');
}
}
public function deleteRelated(Notice $notice)
{
if ($notice->getProfile()->hasRole(Profile_role::DELETED)) {
// Don't save a new Deleted_notice entry if the profile is being deleted
return true;
}
// For auditing purposes, save a record that the notice was deleted.
return Deleted_notice::addNew($notice);
}
/**
* This is run when a 'delete' verb activity comes in.
*
* @return boolean hook flag
*/
protected function saveObjectFromActivity(Activity $act, Notice $stored, array $options=array())
{
// Let's see if this has been deleted already.
$deleted = Deleted_notice::getKV('uri', $act->id);
if ($deleted instanceof Deleted_notice) {
return $deleted;
}
common_debug('DELETING notice: ' . $act->objects[0]->id);
$target = Notice::getByUri($act->objects[0]->id);
$deleted = new Deleted_notice();
$deleted->id = $target->getID();
$deleted->profile_id = $target->getProfile()->getID();
$deleted->uri = Deleted_notice::newUri($target->getProfile(), $target);
$deleted->act_uri = $target->getUri();
$deleted->act_created = $target->created;
$deleted->created = common_sql_now();
common_debug('DELETING notice, storing Deleted_notice entry');
$deleted->insert();
common_debug('DELETING notice, actually deleting now!');
$target->delete();
return $deleted;
}
public function activityObjectFromNotice(Notice $notice)
{
$object = Deleted_notice::fromStored($notice);
return $object->asActivityObject();
}
protected function getActivityForm(ManagedAction $action, $verb, Notice $target, Profile $scoped)
{
if (!$scoped instanceof Profile || !($scoped->sameAs($target->getProfile()) || $scoped->hasRight(Right::DELETEOTHERSNOTICE))) {
throw new AuthorizationException(_('You are not allowed to delete other user\'s notices'));
}
return DeletenoticeForm($action, array('notice'=>$target));
}
}

View File

@ -0,0 +1,224 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
* Copyright (C) 2008, 2009, Control Yourself, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('GNUSOCIAL')) { exit(1); }
/**
* Table Definition for deleted_notice
*/
class Deleted_notice extends Managed_DataObject
{
public $__table = 'deleted_notice'; // table name
public $id; // int(4) primary_key not_null
public $profile_id; // int(4) not_null
public $uri; // varchar(191) unique_key not 255 because utf8mb4 takes more space
public $act_uri; // varchar(191) unique_key not 255 because utf8mb4 takes more space
public $created; // datetime() not_null
public $deleted; // datetime() not_null
public static function schemaDef()
{
return array(
'fields' => array(
'id' => array('type' => 'int', 'not null' => true, 'description' => 'identity of notice'),
'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'author of the notice'),
'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI of the deleted notice'),
'act_uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI of the delete activity, may exist in notice table'),
'act_created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the notice record was created'),
'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the notice record was deleted'),
),
'primary key' => array('id'),
'unique keys' => array(
'deleted_notice_act_uri_key' => array('act_uri'),
),
'indexes' => array(
'deleted_notice_profile_id_idx' => array('profile_id'),
),
);
}
public static function addNew(Notice $notice)
{
$actor = $notice->getProfile();
if ($actor->hasRole(Profile_role::DELETED)) {
// Don't emit notices if the user is deleted
return true;
}
$act = new Activity();
$act->type = ActivityObject::ACTIVITY;
$act->verb = ActivityVerb::DELETE;
$act->time = time();
$act->id = TagURI::mint('deleted_notice:%d:%d:%s',
$actor->getID(),
$notice->getID(),
common_date_iso8601(common_sql_now()));
$act->content = sprintf(_m('<a href="%1$s">%2$s</a> deleted notice <a href="%3$s">{{%4$s}}</a>.'),
htmlspecialchars($actor->getUrl()),
htmlspecialchars($actor->getBestName()),
htmlspecialchars($notice->getUrl()),
htmlspecialchars($notice->getUri())
);
$act->actor = $actor->asActivityObject();
$act->target = new ActivityObject();
$act->target->id = $notice->getUri();
$act->objects = array(clone($act->target));
$url = $notice->getUrl();
$act->selfLink = $url;
$act->editLink = $url;
// This will make ActivityModeration run saveObjectFromActivity which adds
// a new Deleted_notice entry in the database as well as deletes the notice
// if the actor has permission to do so.
$stored = Notice::saveActivity($act, $actor);
return $stored;
}
static public function fromStored(Notice $stored)
{
$class = get_called_class();
$object = new $class;
$object->act_uri = $stored->getUri();
if (!$object->find(true)) {
throw new NoResultException($object);
}
return $object;
}
public function getActor()
{
return Profile::getByID($this->profile_id);
}
static public function getObjectType()
{
return 'activity';
}
protected $_stored = array();
public function getStored()
{
$uri = $this->getTargetUri();
if (!isset($this->_stored[$uri])) {
$stored = new Notice();
$stored->uri = $uri;
if (!$stored->find(true)) {
throw new NoResultException($stored);
}
$this->_stored[$uri] = $stored;
}
return $this->_stored[$uri];
}
public function getTargetUri()
{
return $this->uri;
}
public function getUri()
{
return $this->act_uri;
}
public function asActivityObject(Profile $scoped=null)
{
$actobj = new ActivityObject();
$actobj->id = $this->getUri();
$actobj->type = ActivityUtils::resolveUri(self::getObjectType());
$actobj->actor = $this->getActorObject();
$actobj->target = new ActivityObject();
$actobj->target->id = $this->getTargetUri();
$actobj->objects = array(clone($actobj->target));
$actobj->verb = ActivityVerb::DELETE;
$actobj->title = ActivityUtils::verbToTitle($actobj->verb);
$actor = $this->getActor();
$actobj->content = sprintf(_m('<a href="%1$s">%2$s</a> deleted notice {{%3$s}}.'),
htmlspecialchars($actor->getUrl()),
htmlspecialchars($actor->getBestName()),
htmlspecialchars($actor->getTargetUri())
);
return $actobj;
}
static function newUri(Profile $actor, Managed_DataObject $object, $created=null)
{
if (is_null($created)) {
$created = common_sql_now();
}
return TagURI::mint(strtolower(get_called_class()).':%d:%s:%d:%s',
$actor->getID(),
ActivityUtils::resolveUri(self::getObjectType(), true),
$object->getID(),
common_date_iso8601($created));
}
static public function beforeSchemaUpdate()
{
$table = strtolower(get_called_class());
$schema = Schema::get();
$schemadef = $schema->getTableDef($table);
// 2015-10-03 We change the meaning of the 'uri' field and move its
// content to the 'act_uri' for the deleted activity. act_created is
// added too.
if (isset($schemadef['fields']['act_uri'])) {
// We already have the act_uri field, so no need to migrate to it.
return;
}
echo "\nFound old $table table, upgrading it to contain 'act_uri' and 'act_created' field...";
$schemadef['fields']['act_uri'] = array('type' => 'varchar', 'not null' => true, 'length' => 191, 'description' => 'URI of the delete activity, may exist in notice table');
$schemadef['fields']['act_created'] = array('type' => 'datetime', 'not null' => true, 'description' => 'date the notice record was created');
unset($schemadef['unique keys']);
$schema->ensureTable($table, $schemadef);
$deleted = new Deleted_notice();
$result = $deleted->find();
if ($result === false) {
print "\nFound no deleted_notice entries, continuing...";
return true;
}
print "\nFound $result deleted_notice entries, aligning with new database layout: ";
while($deleted->fetch()) {
$orig = clone($deleted);
$deleted->act_uri = $deleted->uri;
// this is a fake URI just to have something to put there to avoid NULL
$deleted->uri = TagURI::mint(strtolower(get_called_class()).':%d:%s:%s:%s',
$deleted->profile_id,
ActivityUtils::resolveUri(self::getObjectType(), true),
'unknown',
common_date_iso8601($deleted->created));
$deleted->act_created = $deleted->created; // we don't actually know when the notice was created
$deleted->updateWithKeys($orig, 'id');
print ".";
}
print "DONE.\n";
print "Resuming core schema upgrade...";
}
}

View File

@ -88,6 +88,7 @@ function updateSchemaPlugins()
{ {
printfnq("Upgrading plugin schema..."); printfnq("Upgrading plugin schema...");
Event::handle('BeforePluginCheckSchema');
Event::handle('CheckSchema'); Event::handle('CheckSchema');
printfnq("DONE.\n"); printfnq("DONE.\n");