HackerOne users: Testing against this community violates our program's Terms of Service and will result in your bounty being denied.

Anyone know of an addon to ban users from a discussion?

12357

Comments

  • R_JR_J Ex-Fanboy Munich Admin

    Object orientation basics needed

    I'm not the best to explain that and I don't even care if my explanation is 100% complete. You should try to find some beginner tutorials anyway.

    To understand what is going on in Vanilla, you would even need a little bit more than "only" OOP, you also need MVC. Gather some knowledge on that to. Here is my explanation:

    OOP:

    • An object is an instance of a class. You define classes, but in general you cannot directly work with them. You have to create an instance of a class and that instance is called "object" (I will mix the words object and class wildly, but you should know that difference)
    • Classes can inherit properties and methods. Look at your plugins class definition at the top. It extends another class and everything which is defined in that class from which we extend, is usable in your class. You can build an arbitrary chain of extended class. Imagine a class Ford Mustang which extends the Ford class which extends the Automobile class which extends the Vehicle class which extends the MikeOlsonPersonalBelongings class which extends the ThingsInThisUniverse class...
    • An object is nothing more than a bundle of variables and functions. You can do great things with variables, but it comes down to that. The variables which are tied to an object are called properties, the functions defined in an object are called methods
    • Properties and methods can be public or private (or protected, don't care about that right now and just think of them as being special private).
    • When you use an object, you would only be able to use the public properties/methods. But you could inspect the class source code and if you like to understand what is going on "behind the scenes".
    • When you write a class, use protected methods and properties as helpers which a user of your class in general shouldn't ever need to keep the exposed functionality clean.
    • Properties can be thought of as keys in an array: someArray['key'] = 'value'; corresponds to someObject->key = 'value';

    That's the most important, I guess. Really: understanding that an object is an instance of a class is the most important.

    MVC:

    MVC = Model, View, Controller
    Vanilla works with a front-controller: every call to any page is done like that: index.php?p=what/ever (you normally only see /what/ever, but you would be able to change that by setting a config value). That front controller does 1 important job: it passes the parameters "p" value to a router.
    The router splits the call to the following part: controller, method, parameters like e.g. /messages/add/MikeOlson to "MessagesController", method "add()" and parameter "MikeOlson". Internally something similar to this happens:

    $controller = new MessagesController();
    $controller->add('MikeOlson');
    

    And now the MVC explanation:
    "Controller" is just a naming convention for a class which asks the model for data and sends the result to the view. Based on the input (the url parameters like add & MikeOlson in the example above), the controller will use another class called "model" to either retrieve data from the database or save data.
    The model is a class with the only purpose to communicate with the database: getting info from tables (mostly you will find one model per table) or saving info to a table.
    After the controller has gotten info from the model, that data is passed to the view and the controller "renders" it by returning the result of the view to the browser.


    Now look at that:

    $DiscussionModel = new DiscussionModel();
    $Discussion = $DiscussionModel->getID($DiscussionID);
    

    There is a class.discussionmodel.php. The class itself is useless. You need an object: $InstanceOfDiscussionModel = new DiscussionModel();.
    With that object, you can work. One of the methods of the DiscussionModel class is "getID" and that returns an object. If a method returns something, you should have some variable before the equal sign: $Discussion = $DiscussionModel->getID($DiscussionID);
    That $Discussion is a dataset object. You could search for a class.dataset.php and you would find it, showing you what could be done with that dataset.

    For Vanilla you could expect that every Model which is named after an existing table, would return a dataset with all the table columns as properties of the returned table.

    Therefore it is no wonder that you can see the content of the Attributes column with $Discussion->Attributes


    Some words on the Attributes column. It is a special construct in Vanilla. You can save arrays here (in "normal" database columns that isn't possible).
    It works by using PHPs serialize and unserialize functions. But that all happens in the background and you only have to take care for that when you save data (saveToSerializedColumn). But when you read it from what the DiscussionModel returns, Discussion->Attributes is already an array...

  • I am on PHP Version 5.6.30 from what I can tell that should be able to handle the array notation that you use. I need to dig into what's going on there.

  • Making my way through all of this. A lot to digest. I am looking at some OOP and MVC courses presently. At least some beginner get your feet wet type of stuff.

  • R_JR_J Ex-Fanboy Munich Admin

    Good idea. There is a lot of stuff to know about OOP, but you doesn't really need to know much to get started.

  • I have taken a stab at populating the form for a discussion that already had banned users.... Here is what I came up with. I didn't know how to get around looping through the array to get the usernames from the usermodel query.

    <?php
    // Define the plugin:
    $PluginInfo['discussionBan'] = array(
        'Name' => 'Discussion Ban',
        'Description' => 'Grants moderators or permissioned users ability to ban users from specific discussions',
        'Version' => '1.1',
        'RequiredApplications' => array('Vanilla' => '>=2.3'),
        'RequiredTheme' => false,
        'RequiredPlugins' => false,
        'MobileFriendly' => true,
        'HasLocale' => true,
        'License' => 'GNU GPL2',
        'Author' => "Mike Olson",
        'AuthorUrl' => 'https://open.vanillaforums.com/profile/MikeOlson'
    );
    /**
     * Class DiscussionBanPlugin
     *
     * @see http://docs.vanillaforums.com/developers/plugins
     * @see http://docs.vanillaforums.com/developers/plugins/quickstart
     */
    class DiscussionBanPlugin extends Gdn_Plugin {
        /**
         * Add new entry to discussion options.
         *
         * @param GardenController $sender Instance of the calling class.
         * @param mixed $args Event arguments.
         *
         * @return void.
         */
        public function base_discussionOptions_handler($sender, $args) {
            // If user hasn't moderator permissions, we do not want to edit anything.
            if (!Gdn::session()->checkPermission('Garden.Moderation.Manage')) {
                return;
            }
            if (isset($args['DiscussionOptions'])) {
                $args['DiscussionOptions']['discussionBan'] = array(
                    'Label' => t('Ban User from Discussion'),
                    'Url' => 'discussion/discussionBan/'.$args['Discussion']->DiscussionID,
                    'Class' => 'Popup'
                );
            }
        }
        /**
         * Add autocomplete.js to every page.
         *
         * @param GardenController $sender Instance of the calling class.
         *
         * @return void.
         */
        public function base_render_before($sender) {
            $sender->addJsFile('jquery.tokeninput.js');
        }
        /**
         * Show ban user form and save results to discussion attributes.
         *
         * @param PluginController $sender Instance of the calling class
         *
         * @return void.
         */
        public function discussionController_discussionBan_create($sender) {
            $sender->permission('Garden.Moderation.Manage');
            $sender->Form = new Gdn_Form();
            $sender->setData('Title', t('Ban Users From This Discussion'));
            if ($sender->Form->authenticatedPostBack() == false) {
                // This will be run when the view is opened
                //$sender->Form->setValue('UserNames', 'HelloWorld');
                $DiscussionID = $sender->RequestArgs[0];
                $DiscussionModel = new DiscussionModel();
                $Discussion = $DiscussionModel->getID($DiscussionID);
                $Attributes = $Discussion->Attributes;
    
                foreach($Attributes["DiscussionBan"] as $thisId){
                    $UserModel = new UserModel();
                    //$UserModel = new Gdn_Model('User', $Validation);
                    $BannedUserData = $UserModel->GetWhere(array('UserID' => $thisId));
                    $BannedUser = $BannedUserData->FirstRow('', DATASET_TYPE_ARRAY);
                    $BannedNames=$BannedNames.','.$BannedUser->Name;    
                }
                $BannedNames= preg_replace('/,/', '', $BannedNames, 1);
                $sender->Form->setValue('UserNames', $BannedNames);
    
    
            } else {
                // This will only be run when the user pressed the button.
                $userNames = $sender->Form->getFormValue('UserNames', false);
                if ($userNames === false) {
                    return;
                }
                //print_r($userNames)."<br /><br />";
                $userArray = explode(',', $userNames);
                //print_r($userArray)."<br /><br />";
                //print_r($sender->RequestArgs[0]);
                $userIDs = [];
                foreach ($userArray as $name) {
                    $userModel = new UserModel();
                    $user = $userModel->getByUsername($name);
                    $userIDs[] = $user->UserID;
                }
                //print_r($userIDs);
                // wrong: $userIDs = "[".implode(',', $userIDs)."]";
                //print_r($userIDs);
                $discussionModel = new DiscussionModel(); // Since we want to save to discussion table...
                $discussionModel->saveToSerializedColumn(
                    'Attributes', // The column name
                    $sender->RequestArgs[0], // Holds the discussion ID
                    'DiscussionBan', // Internal key
                    $userIDs
                );
                $sender->informMessage(t("Your changes have been saved."));
            }
            $sender->render('discussionBan', '', 'plugins/discussionBan');
        }
    }
    
  • So how this will work now is that when a moderator hits the Ban Users In This Discussion option it will open the form.Any that were previously banned will be prefilled into the text box. The mod could add more names to the text box or remove names from the text box and then save the changes and that will suffice.

    Now I have to get on to the validation piece to keep people that are banned from being able to enter the discussion.

    I have no idea how to do that so I might have to do some digging.

  • R_JR_J Ex-Fanboy Munich Admin

    CommentModel_BeforeSaveComments_Handler would be the name of the method you have to create

  • Hmmm so that is going to keep them from adding to the discussion but would still allow them to view the discussion no? Wondering if there is a method to keep them from even being able to open the discussion.

  • So this is my gameplan:

        public function CommentModel_BeforeSaveComments_Handler($sender) {
            // check to see if session users is on discussion ban list
    
            //if true disable posting
    
            //notify user they are banned from the discussion
        }  
    

    In /applications/vanilla/models/class.commentmodel.php
    I can see public
    function save($FormPostValues, $Settings = false) {
    And there is a beforesavecomment event inside there but I am not sure how to interact with it.

  • R_JR_J Ex-Fanboy Munich Admin

    Some explanations on Vanillas "saving processes". As I've said before there is (generally spoken) one model per table. Without looking at the files you could be sure that there is a class CommentModel with a save() method.

    The way Vanilla allows a plugin author to take influence on data and output is by firing events. There are several events you can expect that they exist. If there is a model which saves something, there normally is a BeforeSaveSomething and an AfterSaveSomething event.

    The save method of a model, besides of writing data to the database, does one addtional and very important task: it validates the input.

    There is a "helper" class which does the validation of fields and an instance of that is tied to the model. You can get an impression of the available validations by looking at that class
    If you attach a rule to a field in your BeforeSave hook, that rule will be checked during the save process. If it fails the check, the result message is added to a property of the validation object - internally it's just an array of messages.

    Instead of adding some rule (some things couldn't be checked with a simple rule) you can also do some checks in your code and based on the result add a validation result to the above mentioned property "directly".
    You wouldn't be able to change that array itself, but there is a method for that.

    The save process validates the input by processing all attached rules and afterwards it checks if there are any validation results. If there are none, the data gets saved. But of if there are any results, they are presented to the user in the red box above the form and the values are not saved.


    Your task would be to check if a user is allowed to comment on the discussion and if not, add a validation result which tells the user that he has been banned and doesn't have the right to post.

    1. The variable $sender is an instance of the CommentModel in this case.
    2. And attached to a model there is an instance of the Validation class.
    3. There is an addValidationResult() method available to manually set errors.

    Try that:

    public function CommentModel_BeforeSaveComment_Handler($sender, $args) {
        $body = strtolower(val('Body', $args['FormPostValues']));
        if (stripos($body, 'cookie')) {
            // We found what we need and so we exit.
            return;
        }
        // No cookies, no saving!
        $sender->Validation->addValidationResult(
            'Body', // Field name
            'I WANT COOKIES!' // Message
        );
    }
    

    And now try saving a comment :wink:

    By the way: val() is a helper function in Vanilla. Sometimes you don't know if you are dealing with an array or an object and in order to get the array value for some key or the property of a nobject, you could use the val function. I would expect that I could simply use $args['FormPostValues']['Body'] above, but since I don't know, I've used val().

    There is no field for the user id. Use InsertUserID when you add a custom message later on because of a "forbidden" user id.

    From the example above you could tell how to get the DiscussionID. You will need it to get a discussion object. Create an instance of the DiscussionModel, get a discussion object with the getID method, check if the session UserID is in the Attributes column. If not return. Otherwise echo a meaningful message.

    Next step finished. =)

  • Had a family situation that I had to deal with but getting back to this now.

  • Ok R_J so in the last bit of code you offered I put it in place and even though I am signed in I get the "I want Cookies" error.

  • R_JR_J Ex-Fanboy Munich Admin

    That is exactly what should happen. Read the code and you will find the magic word that will allow you to save a comment.

    Only if you cannot find a solution look here for "inspiration": https://en.m.wikipedia.org/wiki/Cookie_Monster_(computer_program)

    The only purpose of my code was to demonstrate you how to manually create errors because that is needed to disallow posting for banned users

  • rbrahmsonrbrahmson "You may say I'm a dreamer / But I'm not the only one" NY ✭✭✭

    Oh, I think Vanilla shouldn't save any discussion unless it contains a cookie!
    Feed me, feed me...

  • Ok so this is my stab at it but I am not preventing the user from replying.


  • R_JR_J Ex-Fanboy Munich Admin

    @MikeOlson said:
    Ok so this is my stab at it but I am not preventing the user from replying.


    There really is no need for making $Attributes its own variable. It's not that much shorter and adding another variable takes more memory, but doesn't speed up anything. If you would save the result of a method to a variable, it could make sense, but saving the property of an object into its own variable is a waste of resources.

              foreach($Attributes["DiscussionBan"] as $thisId){
                  if($UserID==$thisId){
                      $banned++;
                  }
              }
    

    For discussions where there are no banned users, $Attributes['DiscussionBan'] is not defined. You should handle that in order to avoid errors:

    if (!isset($Discussion->Attributes['DiscussionBan']) {
        return;
    }
    

    What you do in order to find out if the session user is in the array of banned users, is to loop through the array. PHP has a quite a lot of array functions which make such tasks a breez. Look at the in_array function to see how you can simply check if the session user id is in the array of banned users and return if not. Your complete $banned counter and foreach construct could be replaced with three lines that way.

    If you return when the user id is not in the array, you do not really need to write an else construct. Simply in the next step of the program flow would be the "addValidationResult" part.

    But what did you meant by " I am not preventing the user from replying"?

  • Ok so I have a test user account (userid 16) that I have banned from from discussionid 400. Here is the value in attributes for discussionid 400:
    a:1:{s:13:"DiscussionBan";a:2:{i:0;i:16;i:1;i:4;}}

    Here is the validation before save comment that I am trying to use:

        public function CommentModel_BeforeSaveComments_Handler($sender, $args) {   
                $DiscussionID = $args['Discussion']->DiscussionID;
                $DiscussionModel = new DiscussionModel();
                $Discussion = $DiscussionModel->getID($DiscussionID);
                //$Attributes = $Discussion->Attributes;
    
                    if (!isset($Discussion->Attributes['DiscussionBan'])) {
                        return;
                    } 
    
    
                        if (in_array($Session->User->ID, $Discussion->Attributes['DiscussionBan'])) {
                            $sender->Validation->addValidationResult(
                                    'Body', // Field name
                                    'You have been banned from participating in this discussion.' // Message
                                );
    
                        } 
                }
    

    This doesn't seem to stop that user from posting to the topic. In fact, if I do the following I would think nobody could post to any topic but the validation isn't stopping a user from posting:

        public function CommentModel_BeforeSaveComments_Handler($sender, $args) {   
        $sender->Validation->addValidationResult(
            'Body', // Field name
            'You have been banned from participating in this discussion.' // Message
            );
        }
    
  • R_JR_J Ex-Fanboy Munich Admin

    @MikeOlson said:
    Ok so I have a test user account (userid 16) that I have banned from from discussionid 400. Here is the value in attributes for discussionid 400:
    a:1:{s:13:"DiscussionBan";a:2:{i:0;i:16;i:1;i:4;}}

    Here is the validation before save comment that I am trying to use:

        public function CommentModel_BeforeSaveComments_Handler($sender, $args) { 
                $DiscussionID = $args['Discussion']->DiscussionID;
                $DiscussionModel = new DiscussionModel();
                $Discussion = $DiscussionModel->getID($DiscussionID);
              //$Attributes = $Discussion->Attributes;
    
                  if (!isset($Discussion->Attributes['DiscussionBan'])) {
                      return;
                  } 
    
    
                      if (in_array($Session->User->ID, $Discussion->Attributes['DiscussionBan'])) {
    

    The variable $Session is not defined here and the user object doesn't have a property ID, it is called UserID. Try
    if (in_array(Gdn::session()->UserID, $Discussion->Attributes['DiscussionBan'])) {'

    The rest looks good to me.

  • Still doesn't stop a banned user from posting in the discussion:

        public function CommentModel_BeforeSaveComments_Handler($sender, $args) {   
                $DiscussionID = $args['Discussion']->DiscussionID;
                $DiscussionModel = new DiscussionModel();
                $Discussion = $DiscussionModel->getID($DiscussionID);
    
                    if (!isset($Discussion->Attributes['DiscussionBan'])) {
                        return;
                    } 
    
                        if (in_array(Gdn::session()->UserID, $Discussion->Attributes['DiscussionBan'])){
                            $sender->Validation->addValidationResult(
                                    'Body', // Field name
                                    'You have been banned from participating in this discussion.' // Message
                                );
    
                        } 
                }
    
  • R_JR_J Ex-Fanboy Munich Admin

    There is a typo here: public function CommentModel_BeforeSaveComments_Handler($sender, $args) {
    Must be
    public function CommentModel_BeforeSaveComment_Handler($sender, $args) {

Sign In or Register to comment.