# Adding a ruby controller via plugin

**URL:** https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661
**Category:** Development
**Created:** [April 8, 2017, 8:28pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661 "2017-04-08T20:28:11Z")
**Posts on this page:** 15
**Page:** 1

<div class="post-metadata">

### Author: ![jomaxro](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jomaxro/32/126216_2.png) [@jomaxro](https://meta.discourse.org/u/jomaxro)
#### Post date: [April 8, 2017, 8:28pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/1 "2017-04-08T20:28:11Z")

</div>

_Please bare with me here - first time developing a Discourse plugin. Was thrown into this project by my job…_

**Background:**  
This is a continuation of [this post](https://meta.discourse.org/t/change-last-read-post-number-for-user-via-api/60454). For clarity, I’ll repeat the background here.

My company runs an internal Discourse Forum, as well as an internal WordPress site. The forums are configured to use SSO from the WordPress instance. On the forums, there is a category called “Updates”. On the WordPress side, when a user logs in there is the following PHP function:

```plaintext
$results = $this->_getRequest("c/updates.json", null, $username);

```

The `$results` are then parsed. For each topic, if the `last_read_post_number < highest_post_number` then we assume the user hasn’t fully read the topic and display an excerpt of the topic in a div on the WordPress site. In the div, there is an `X`. The intention is that clicking the `X` will set the user’s `last_read_post_number` equal to the `highest_post_number` so it is “read” in the forums too.

This was initially set up over a year ago by adding a custom controller and route to make the `last_read_post_number`. Problem is the route was set in a way that overwrote the Discourse `routes.rb` file, causing intermittent and eventually permanent problems. In the old topic I asked if there was a way to do this via the API, so I could integrate the code into WordPress. After chatting with engineering, it was determined that the PHP/JS code for the alerts were written by a previous contractor and were not going to be touched as they currently work.

As such, I’m now looking to recreate the old setup “properly”, which I’m assuming means a plugin. The old setup added two files via `app.yml`

```plaintext
run:
  - exec: echo "Beginning of custom commands"
  - replace:
      filename: "/etc/nginx/conf.d/discourse.conf"
      from: /client_max_body_size.+$/
      to: client_max_body_size 512m;
  - exec: cp /shared/routes.rb /var/www/discourse/config/routes.rb
  - exec: cp /shared/staff_corner_controller.rb /var/www/discourse/app/controllers/staff_corner_controller.rb

```

**`/shared/routes.rb`**  
A 1±year-old copy of the Discourse `routes.rb` file with `get "staffcorner/mark-read/:pulseUserId/:topicId" => "staffcorner#mark_read"` added to the `Discourse::Application.routes.draw do` section.

**`/shared/staff_corner_controller.rb`**

```plaintext
class StaffcornerController < ApplicationController
        skip_before_filter :check_xhr, :redirect_to_login_if_required
        skip_before_filter :require_login
    def mark_read
        pulseUserId = params[:pulseUserId]
        topicId = params[:topicId]
        sso_record = SingleSignOnRecord.where(external_id: pulseUserId).first
        #wsUrl = SiteSetting.pulse_ws_url
        # No record yet, so create it
        if (!sso_record)
            # Load Pulse roles
            res = Net::HTTP.post_form(URI('http://webservices.idtech.com/StaffCorner.asmx/GetUserInfoByUserID'), {'Authenticate' => '{REDACTED}', 'companyID' => '1', 'userID' => pulseUserId})
            data = XmlSimple.xml_in(res.body)
            userData = data['diffgram'][0]['Result'][0]['User'][0]
            email = userData['Email'][0]
            username = email
            name = userData['FirstName'][0]
            user = User.where(:email => email).first
        end
        user = user || sso_record.user
        topic = Topic.find(topicId)
        if (!topic)
                render :json => {success: false, message: "Unknown topic"}
                return
        end
        topicUser = TopicUser.where(:user_id => user.id, :topic_id => topic.id).first
        if (!topicUser)
                topicUser = TopicUser.create(:user => user, :topic => topic, :last_read_post_number => topic.posts.last)
        end
        topicUser.last_read_post_number = topic.posts.last.id
        topicUser.save
        render :json => {success: true}
    end
end

```

* * *

I’ve tried implementing this as a plugin, but I’m not suceeding… Here’s what I’ve got at the moment.

**Structure:**

```plaintext
/staff-forum-plugin
..plugin.rb
../app
..../controllers
....../staff_corner_controller.rb
../config
....settings.yml
..../locales
......server.en.yml

```

**plugin.rb:**

```plaintext
# name: staff-forum-plugin
# about: Adds a route for staff corner to mark topics as read
# version: 0.1.0
# authors: Joshua Rosenfeld
# url: {Internal}
enabled_site_setting :mark_read_enabled
after_initialize do
    module ::StaffForumsMarkRead
        class Engine < ::Rails::Engine
            engine_name "staff_forums_mark_read"
            isolate_namespace StaffForumsMarkRead
        end
    end
    
    require_dependency 'application_controller'
    class StaffForumsMarkRead::MarkReadController < ::ApplicationController
        before_action :ensure_plugin_enabled
        def ensure_plugin_enabled
            raise Discourse::InvalidAccess.new('Staff Forums Mark Read plugin is not enabled') if !SiteSetting.mark_read_enabled
        end
    end
    
    Discourse::Application.routes.append do
        get "staffcorner/mark-read/:pulseUserId/:topicId" => "staffcorner#mark_read"
    end
end

```

**staff\_corner\_controller.rb:**  
Same as above

**settings.yml:**

```plaintext
plugins:
  mark_read_enabled:
    default: true
    client: true

```

**server.en.yml:**

```plaintext
en:
    site_settings:
        mark_read_enabled: "Allow Staff Corner important updates banner to mark topics as read?"

```

* * *

Any thoughts, ideas, etc. would be much appreciated. This worked (at least it accomplished what it was supposed to do) when the files were copied as part the `app.yml` custom commands - but doing this broke the site, so I just need to figure out how to add the files the correct way.

---

<div class="post-metadata">

### Author: ![gdpelican](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/gdpelican/32/81308_2.png) [@gdpelican](https://meta.discourse.org/u/gdpelican)
#### Post date: [April 8, 2017, 8:39pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/2 "2017-04-08T20:39:37Z")

</div>

Something about what’s not working would really help here; it looks like you’re on the right track.

I’d recommend you have a peek at some of the existing plugins which do this sort of thing; [Retort](http://www.github.com/gdpelican/retort) is a pretty minimal example of adding a single route and controller to Discourse.

---

<div class="post-metadata">

### Author: ![jomaxro](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jomaxro/32/126216_2.png) [@jomaxro](https://meta.discourse.org/u/jomaxro)
#### Post date: [April 8, 2017, 8:53pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/3 "2017-04-08T20:53:31Z")

</div>

Oops, that would help, wouldn’t it! Completely forgot after writing all the details…

When testing this on our dev site, after clicking the `X` (which calls the URL) the following error is seen in the console:

```plaintext
XMLHttpRequest cannot load https://staffforum-dev.{redacted}.com/staffcorner/mark-read/637539/43. 
Redirect from 'https://staffforum-dev.{redacted}.com/staffcorner/mark-read/637539/43' to
'https://staffforum-dev.{redacted}.com/session/sso' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource. 
Origin 'https://staffdev.{redacted}.com' is therefore not allowed access.

```

Edit:

> [@gdpelican](#):
>
> I’d recommend you have a peek at some of the existing plugins which do this sort of thing

Yeah, I created this as is by reviewing @eviltrout’s plugin guides, and a number of existing plugins. Looking at Retort, I can’t seem to find a ruby controller file anywhere…

---

<div class="post-metadata">

### Author: ![gdpelican](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/gdpelican/32/81308_2.png) [@gdpelican](https://meta.discourse.org/u/gdpelican)
#### Post date: [April 8, 2017, 9:15pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/4 "2017-04-08T21:15:15Z")

</div>

Oh, you have a CORS issue, looks like between `https://staffforum-dev.{redacted}.com` and `https://staffdev.{redacted}.com`. ([Moar on CORS](http://stackoverflow.com/questions/10636611/how-does-access-control-allow-origin-header-work))

If I had to guess, I’d say you need to modify your `https://staffdev.{redacted}.com` site to allow requests from `https://staffforum-dev.{redacted}.com` via the `ACCESS-CONTROL-ALLOW-ORIGIN` header. Other than that, your plugin is probably working mostly as you intend.

---

<div class="post-metadata">

### Author: ![jomaxro](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jomaxro/32/126216_2.png) [@jomaxro](https://meta.discourse.org/u/jomaxro)
#### Post date: [April 8, 2017, 9:18pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/5 "2017-04-08T21:18:55Z")

</div>

Thanks @gdpelican, I’m reading more on CORS now. Just to be sure I’m following you correctly - I need to modify `staffdev` (WordPress) to allow requests from `staffforum-dev` (Discourse)? Not the other way around?

Edit: Also, I’m having a hard time understanding why this worked previously (with the copied in route and controller) and doesn’t now if my plugin does in-fact replicate the old setup.

---

<div class="post-metadata">

### Author: ![david](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/david/32/157490_2.png) [@david](https://meta.discourse.org/u/david)
#### Post date: [April 8, 2017, 9:30pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/6 "2017-04-08T21:30:23Z")

</div>

If I’m reading the error right, I think you’re doing an AJAX request FROM `staffdev.{redacted}.com` TO `staffforumdev.{redacted}.com`. Therefore you need to modify Discourse to allow requests from staffdev.

~~I believe that should be as simple as adding staffdev to the “allowed embed” list in the admin panel at `/admin/customize/embedding`~~

---

<div class="post-metadata">

### Author: ![david](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/david/32/157490_2.png) [@david](https://meta.discourse.org/u/david)
#### Post date: [April 8, 2017, 9:34pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/7 "2017-04-08T21:34:31Z")

</div>

Actually, the ‘embed’ section seems to be for embedding by iFrame.

The `cors origins` site setting might be more useful, and looks like you’ll need to “set the DISCOURSE\_ENABLE\_CORS env variable”

---

<div class="post-metadata">

### Author: ![jomaxro](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jomaxro/32/126216_2.png) [@jomaxro](https://meta.discourse.org/u/jomaxro)
#### Post date: [April 8, 2017, 9:38pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/8 "2017-04-08T21:38:00Z")

</div>

> [@david](#):
>
> Actually, the ‘embed’ section seems to be for embedding by iFrame.

I was just writing a post to say that didn’t work.

> [@david](#):
>
> looks like you’ll need to “set the DISCOURSE\_ENABLE\_CORS env variable”

And now the testing stops for the weekend…I don’t have server access, so further testing will need to wait until I can talk to IT.

If you by chance have an answer to my question [above](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/5) I’d appreciate that.

---

<div class="post-metadata">

### Author: ![david](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/david/32/157490_2.png) [@david](https://meta.discourse.org/u/david)
#### Post date: [April 8, 2017, 9:40pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/9 "2017-04-08T21:40:48Z")

</div>

> [@jomaxro](#):
>
> why this worked previously

My only guess would be that the previous implementation called the Discourse API from a php script, rather than from javascript in the browser. Therefore it wouldn’t be subject to CORS checks.

---

<div class="post-metadata">

### Author: ![jomaxro](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jomaxro/32/126216_2.png) [@jomaxro](https://meta.discourse.org/u/jomaxro)
#### Post date: [April 8, 2017, 9:42pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/10 "2017-04-08T21:42:34Z")

</div>

> [@david](#):
>
> the previous implementation called the Discourse API from a php script, rather than from javascript in the browser.

Nope - nothing has changed on the Wordpress side. Give me a few minutes and I can grab the implementation there. The only change is that instead of copying a modified routes file and controller via the run section of the app.yml, I’m now trying to do it as a plugin.

Edit: Here’s the relevant PHP file (that hasn’t changed):

```plaintext
<?php
/**
 * Staff Corner forum alert system.
 */
global $forumHelper;
global $forumUser;
global $sessionUser;
global $forumUsername;
$alerts = $forumHelper && $sessionUser ? $forumHelper->getUnreadAlerts($sessionUser->getUserId(), $forumUsername) : array();
if (count($alerts) > 0) :
    usort($alerts, function($a, $b) {
        return $a->bumped_at > $b->bumped_at ? -1 : $b->bumped_at < $a->bumped_at ? 1 : 0;
    });
?>
<script type="text/javascript">
    jQuery(function() {
        var $ = $ || jQuery;
        $('.alerts .dismiss a').click(function(e) {
            $(this).parents('.alerts li').remove();
            $.ajax($(this).attr('href'));
            e.preventDefault();
        });
    });
</script>
<div class="alerts full-width frame">
    <ul>
    <?php for ($i = 0, $len = count($alerts); $i < $len; $i++) :
        $alert = $alerts[$i];
        $date = new DateTime($alert->bumped_at);
        $now = new DateTime();
        $diff = $now->getTimestamp() - $date->getTimestamp();
        if ($diff < 60 * 60) { // minutes
            $hoursAgo = floor($diff / 60) . " minutes";
        } else if ($diff < 60 * 60 * 24) { // hours
            $hoursAgo = floor($diff / (60 * 60)) . " hours";
        } else { // days
            $hoursAgo = floor($diff / (60 * 60 * 24)) . " days";
        }
    ?>
        <li style="z-index: <?php echo $len - $i + 1 ?>">
            <h1><?php echo $alert->title; ?></h1>
            <p class="snippet"><?php echo $alert->msg; ?></p><a href="<?php echo $alert->url; ?>">See More</a>
            <p class="byline"><?php echo $hoursAgo ?> ago from <?php echo $alert->author ?></p>
            <p class="dismiss"><a href="<?php echo $alert->mark_read_url ?>">X</a></p>
        </li>
    <?php endfor; ?>
    </ul>
</div>
<?php endif; ?>

```

---

<div class="post-metadata">

### Author: ![david](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/david/32/157490_2.png) [@david](https://meta.discourse.org/u/david)
#### Post date: [April 8, 2017, 10:36pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/11 "2017-04-08T22:36:18Z")

</div>

Sorry, I’m probably making more confusion out of this than actually helping 😉

This bit should mean that CORS is not an issue:

> [@jomaxro](#):
>
> ```ruby
> skip_before_filter :check_xhr, :redirect_to_login_if_required
> 
> ```

However, in your error it says

> [@jomaxro](#):
>
> ```plaintext
> Redirect from 'https://staffforum-dev.{redacted}.com/staffcorner/mark-read/637539/43' to
> 'https://staffforum-dev.{redacted}.com/session/sso
> 
> ```

which suggests that a redirect is happening to the login page… That shouldn’t be happening, because the controller says `skip_before_filter :check_xhr, :redirect_to_login_if_required`.

As for why that’s the case, I don’t know from just reading the code, sorry! Hopefully someone more experienced will be able to spot something.

---

<div class="post-metadata">

### Author: ![jomaxro](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jomaxro/32/126216_2.png) [@jomaxro](https://meta.discourse.org/u/jomaxro)
#### Post date: [April 8, 2017, 10:42pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/12 "2017-04-08T22:42:51Z")

</div>

> [@david](#):
>
> That shouldn’t be happening, because the controller says skip\_before\_filter :check\_xhr, :redirect\_to\_login\_if\_required.

Question: If the controller wasn’t routable, the `skip_before_filter` line wouldn’t be run. With the site being login\_required, hitting any url should direct to login. Could that be happening here? Theoretically?

---

<div class="post-metadata">

### Author: ![david](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/david/32/157490_2.png) [@david](https://meta.discourse.org/u/david)
#### Post date: [April 8, 2017, 11:01pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/13 "2017-04-08T23:01:13Z")

</div>

That sounds like a possibility

I’m not sure whether things in `plugins/{name}/app/controllers` are loaded automatically. You might need to “require” them, for example:

> <https://github.com/discourse/discourse-slack-official/blob/master/plugin.rb#L25>

---

<div class="post-metadata">

### Author: ![jomaxro](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jomaxro/32/126216_2.png) [@jomaxro](https://meta.discourse.org/u/jomaxro)
#### Post date: [April 8, 2017, 11:13pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/14 "2017-04-08T23:13:21Z")

</div>

I think that did it ❣ Doing some tests to double check…

---

<div class="post-metadata">

### Author: ![jomaxro](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jomaxro/32/126216_2.png) [@jomaxro](https://meta.discourse.org/u/jomaxro)
#### Post date: [April 8, 2017, 11:25pm UTC](https://meta.discourse.org/t/adding-a-ruby-controller-via-plugin/60661/15 "2017-04-08T23:25:13Z")

</div>

OK, testing complete. It seems to be working as expected @david, thanks!

There is still a console error, but it doesn’t seem to be preventing the plugin from doing what I want (making the banner go away in WordPress).

```plaintext
XMLHttpRequest cannot load https://staffforum-dev.{redacted}.com/staffcorner/mark-read/637539/47.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'https://staffdev.idtech.com' is therefore not allowed access.

```
