From 88f7bb1ed5faded5563eeb1d75d5fb44126b0712 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 3 Oct 2015 02:02:37 +0200 Subject: [PATCH] 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. --- classes/Deleted_notice.php | 54 ----- classes/Notice.php | 27 +-- db/core.php | 1 - lib/default.php | 1 + .../ActivityModerationPlugin.php | 118 +++++++++ .../classes/Deleted_notice.php | 224 ++++++++++++++++++ .../ActivityModeration/forms/deletenotice.php | 0 scripts/upgrade.php | 1 + 8 files changed, 346 insertions(+), 80 deletions(-) delete mode 100644 classes/Deleted_notice.php create mode 100644 plugins/ActivityModeration/ActivityModerationPlugin.php create mode 100644 plugins/ActivityModeration/classes/Deleted_notice.php rename lib/deletenoticeform.php => plugins/ActivityModeration/forms/deletenotice.php (100%) diff --git a/classes/Deleted_notice.php b/classes/Deleted_notice.php deleted file mode 100644 index 23bbea1bab..0000000000 --- a/classes/Deleted_notice.php +++ /dev/null @@ -1,54 +0,0 @@ -. - */ - -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'), - ), - ); - } -} diff --git a/classes/Notice.php b/classes/Notice.php index 44d5adb1b7..737f186a10 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -163,34 +163,11 @@ class Notice extends Managed_DataObject if ($this->getProfile()->sameAs($actor) || $actor->hasRight(Right::DELETEOTHERSNOTICE)) { 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))) { // Clear related records diff --git a/db/core.php b/db/core.php index d779717fd4..f654d79d99 100644 --- a/db/core.php +++ b/db/core.php @@ -72,7 +72,6 @@ $classes = array('Schema_version', 'Group_block', 'Group_alias', 'Session', - 'Deleted_notice', 'Config', 'Profile_role', 'Location_namespace', diff --git a/lib/default.php b/lib/default.php index 38b8bcb1af..554d3ae63c 100644 --- a/lib/default.php +++ b/lib/default.php @@ -306,6 +306,7 @@ $default = array('core' => array( 'ActivityVerb' => array(), 'ActivityVerbPost' => array(), + 'ActivityModeration' => array(), 'AuthCrypt' => array(), 'Cronish' => array(), 'Favorite' => array(), diff --git a/plugins/ActivityModeration/ActivityModerationPlugin.php b/plugins/ActivityModeration/ActivityModerationPlugin.php new file mode 100644 index 0000000000..1494833992 --- /dev/null +++ b/plugins/ActivityModeration/ActivityModerationPlugin.php @@ -0,0 +1,118 @@ + + */ +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)); + } +} diff --git a/plugins/ActivityModeration/classes/Deleted_notice.php b/plugins/ActivityModeration/classes/Deleted_notice.php new file mode 100644 index 0000000000..c450c0eb63 --- /dev/null +++ b/plugins/ActivityModeration/classes/Deleted_notice.php @@ -0,0 +1,224 @@ +. + */ + +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('%2$s deleted notice {{%4$s}}.'), + 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('%2$s 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..."; + } + +} diff --git a/lib/deletenoticeform.php b/plugins/ActivityModeration/forms/deletenotice.php similarity index 100% rename from lib/deletenoticeform.php rename to plugins/ActivityModeration/forms/deletenotice.php diff --git a/scripts/upgrade.php b/scripts/upgrade.php index 126ef29036..06a2f74771 100755 --- a/scripts/upgrade.php +++ b/scripts/upgrade.php @@ -88,6 +88,7 @@ function updateSchemaPlugins() { printfnq("Upgrading plugin schema..."); + Event::handle('BeforePluginCheckSchema'); Event::handle('CheckSchema'); printfnq("DONE.\n");