From 20c9a312c7d957ba9d02cd2b4cf6a87b74e54325 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Sun, 22 Feb 2015 20:47:18 +0100
Subject: [PATCH] FIX: clicks counter on attachments wasn't always working

---
 .../javascripts/discourse/lib/click_track.js  | 15 +++---
 .../javascripts/discourse/views/post.js.es6   | 42 ++++++++-------
 .../javascripts/discourse/views/topic.js.es6  |  2 +-
 app/controllers/clicks_controller.rb          |  6 ---
 app/models/topic_link_click.rb                | 52 ++++++++++++-------
 lib/url_helper.rb                             |  2 +-
 spec/controllers/clicks_controller_spec.rb    | 26 +++-------
 7 files changed, 70 insertions(+), 75 deletions(-)

diff --git a/app/assets/javascripts/discourse/lib/click_track.js b/app/assets/javascripts/discourse/lib/click_track.js
index f53d0e1ed..9672c860e 100644
--- a/app/assets/javascripts/discourse/lib/click_track.js
+++ b/app/assets/javascripts/discourse/lib/click_track.js
@@ -5,6 +5,7 @@
   @namespace Discourse
   @module Discourse
 **/
+
 Discourse.ClickTrack = {
 
   /**
@@ -43,15 +44,11 @@ Discourse.ClickTrack = {
     if (!ownLink) {
       var $badge = $('span.badge', $link);
       if ($badge.length === 1) {
-        // don't update counts in category badge
-        if ($link.closest('.badge-category').length === 0) {
-          // nor in oneboxes (except when we force it)
-          if (($link.closest(".onebox-result").length === 0 && $link.closest('.onebox-body').length === 0) || $link.hasClass("track-link")) {
-            var html = $badge.html();
-            if (/^\d+$/.test(html)) {
-              $badge.html(parseInt(html, 10) + 1);
-            }
-          }
+        // don't update counts in category badge nor in oneboxes (except when we force it)
+        if ($link.hasClass("track-link") ||
+            $link.closest('.badge-category,.onebox-result,.onebox-body').length === 0) {
+          var html = $badge.html();
+          if (/^\d+$/.test(html)) { $badge.html(parseInt(html, 10) + 1); }
         }
       }
     }
diff --git a/app/assets/javascripts/discourse/views/post.js.es6 b/app/assets/javascripts/discourse/views/post.js.es6
index 422771c06..42bf9dfc4 100644
--- a/app/assets/javascripts/discourse/views/post.js.es6
+++ b/app/assets/javascripts/discourse/views/post.js.es6
@@ -48,12 +48,12 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
     Em.run.scheduleOnce('afterRender', this, '_cookedWasChanged');
   }.observes('post.cooked'),
 
-  _cookedWasChanged: function() {
+  _cookedWasChanged() {
     this.trigger('postViewUpdated', this.$());
     this._insertQuoteControls();
   },
 
-  mouseUp: function(e) {
+  mouseUp(e) {
     if (this.get('controller.multiSelect') && (e.metaKey || e.ctrlKey)) {
       this.get('controller').toggledSelectedPost(this.get('post'));
     }
@@ -74,7 +74,7 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
 
   repliesShown: Em.computed.gt('post.replies.length', 0),
 
-  _updateQuoteElements: function($aside, desc) {
+  _updateQuoteElements($aside, desc) {
     var navLink = "",
         quoteTitle = I18n.t("post.follow_quote"),
         postNumber = $aside.data('post');
@@ -108,7 +108,7 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
     $('.quote-controls', $aside).html(expandContract + navLink);
   },
 
-  _toggleQuote: function($aside) {
+  _toggleQuote($aside) {
     if (this.get('expanding')) { return; }
     this.set('expanding', true);
 
@@ -151,23 +151,29 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
   },
 
   // Show how many times links have been clicked on
-  _showLinkCounts: function() {
-    var self = this,
-        link_counts = this.get('post.link_counts');
+  _showLinkCounts() {
+    const self = this,
+          link_counts = this.get('post.link_counts');
 
-    if (!link_counts) return;
+    if (!link_counts) { return; }
 
     link_counts.forEach(function(lc) {
-      if (!lc.clicks || lc.clicks < 1) return;
+      if (!lc.clicks || lc.clicks < 1) { return; }
 
       self.$(".cooked a[href]").each(function() {
-        var link = $(this);
-        if ((!lc.internal || lc.url[0] === "/") && link.attr('href') === lc.url) {
-          // don't display badge counts on category badge
-          if (link.closest('.badge-category').length === 0 && ((link.closest(".onebox-result").length === 0 && link.closest('.onebox-body').length === 0) || link.hasClass("track-link"))) {
-            link.append("<span class='badge badge-notification clicks' title='" +
-                        I18n.t("topic_map.clicks", {count: lc.clicks}) +
-                        "'>" + Discourse.Formatter.number(lc.clicks) + "</span>");
+        const $link = $(this),
+              href = $link.attr('href');
+
+        let valid = !lc.internal && href === lc.url;
+
+        // this might be an attachment
+        if (lc.internal) { valid = href.indexOf(lc.url) >= 0; }
+
+        if (valid) {
+          // don't display badge counts on category badge & oneboxes (unless when explicitely stated)
+          if ($link.hasClass("track-link") ||
+              $link.closest('.badge-category,.onebox-result,.onebox-body').length === 0) {
+            $link.append("<span class='badge badge-notification clicks' title='" + I18n.t("topic_map.clicks", {count: lc.clicks}) + "'>" + Discourse.Formatter.number(lc.clicks) + "</span>");
           }
         }
       });
@@ -176,7 +182,7 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
 
   actions: {
     // Toggle the replies this post is a reply to
-    toggleReplyHistory: function(post) {
+    toggleReplyHistory(post) {
 
       var replyHistory = post.get('replyHistory'),
           topicController = this.get('controller'),
@@ -227,7 +233,7 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
   },
 
   // Add the quote controls to a post
-  _insertQuoteControls: function() {
+  _insertQuoteControls() {
     var self = this,
         $quotes = this.$('aside.quote');
 
diff --git a/app/assets/javascripts/discourse/views/topic.js.es6 b/app/assets/javascripts/discourse/views/topic.js.es6
index 758a8a06e..1050dc5a3 100644
--- a/app/assets/javascripts/discourse/views/topic.js.es6
+++ b/app/assets/javascripts/discourse/views/topic.js.es6
@@ -51,8 +51,8 @@ var TopicView = Discourse.View.extend(AddCategoryClass, Discourse.Scrolling, {
 
       var $target = $(e.target);
       if ($target.hasClass('mention') || $target.parents('.expanded-embed').length) { return false; }
-      return Discourse.ClickTrack.trackClick(e);
 
+      return Discourse.ClickTrack.trackClick(e);
     });
 
   }.on('didInsertElement'),
diff --git a/app/controllers/clicks_controller.rb b/app/controllers/clicks_controller.rb
index 056a8e84d..8472c9e7f 100644
--- a/app/controllers/clicks_controller.rb
+++ b/app/controllers/clicks_controller.rb
@@ -8,12 +8,6 @@ class ClicksController < ApplicationController
     if params[:topic_id].present? || params[:post_id].present?
       params.merge!({ user_id: current_user.id }) if current_user.present?
       @redirect_url = TopicLinkClick.create_from(params)
-
-      if @redirect_url.blank? && params[:url].index('?')
-        # Check the url without query parameters
-        params[:url].sub!(/\?.*$/, '')
-        @redirect_url = TopicLinkClick.create_from(params)
-      end
     end
 
     # Sometimes we want to record a link without a 302. Since XHR has to load the redirected
diff --git a/app/models/topic_link_click.rb b/app/models/topic_link_click.rb
index e06ad2ce7..008263567 100644
--- a/app/models/topic_link_click.rb
+++ b/app/models/topic_link_click.rb
@@ -1,5 +1,10 @@
 require_dependency 'discourse'
 require 'ipaddr'
+require 'url_helper'
+
+class TopicLinkClickHelper
+  include UrlHelper
+end
 
 class TopicLinkClick < ActiveRecord::Base
   belongs_to :topic_link, counter_cache: :clicks
@@ -10,15 +15,27 @@ class TopicLinkClick < ActiveRecord::Base
 
   # Create a click from a URL and post_id
   def self.create_from(args={})
+    url = args[:url]
+    return nil if url.blank?
 
-    # If the URL is absolute, allow HTTPS and HTTP versions of it
-    if args[:url] =~ /^http/
-      http_url = args[:url].sub(/^https/, 'http')
-      https_url = args[:url].sub(/^http\:/, 'https:')
-      link = TopicLink.select([:id, :user_id]).where('url = ? OR url = ?', http_url, https_url)
-    else
-      link = TopicLink.select([:id, :user_id]).where(url: args[:url])
+    helper = TopicLinkClickHelper.new
+    uri = URI.parse(url) rescue nil
+
+    urls = Set.new
+    urls << url
+    if url =~ /^http/
+      urls << url.sub(/^https/, 'http')
+      urls << url.sub(/^http:/, 'https:')
+      urls << helper.schemaless(url)
     end
+    urls << helper.absolute_without_cdn(url)
+    urls << uri.path if uri.try(:host) == Discourse.current_hostname
+    urls << url.sub(/\?.*$/, '') if url.include?('?')
+
+    link = TopicLink.select([:id, :user_id])
+
+    # test for all possible URLs
+    link = link.where(Array.new(urls.count, "url = ?").join(" OR "), *urls)
 
     # Find the forum topic link
     link = link.where(post_id: args[:post_id]) if args[:post_id].present?
@@ -27,23 +44,18 @@ class TopicLinkClick < ActiveRecord::Base
     link = link.where(topic_id: args[:topic_id]) if args[:topic_id].present?
     link = link.first
 
-    # If no link is found, return the url for relative links
+    # If no link is found...
     unless link.present?
-      return args[:url] if args[:url] =~ /^\//
+      # ... return the url for relative links or when using the same host
+      return url if url =~ /^\// || uri.try(:host) == Discourse.current_hostname
 
-      begin
-        uri = URI.parse(args[:url])
-        return args[:url] if uri.host == URI.parse(Discourse.base_url).host
-      rescue
-      end
-
-      # If we have it somewhere else on the site, just allow the redirect. This is
-      # likely due to a onebox of another topic.
-      link = TopicLink.find_by(url: args[:url])
+      # If we have it somewhere else on the site, just allow the redirect.
+      # This is likely due to a onebox of another topic.
+      link = TopicLink.find_by(url: url)
       return link.present? ? link.url : nil
     end
 
-    return args[:url] if (args[:user_id] && (link.user_id == args[:user_id]))
+    return url if args[:user_id] && link.user_id == args[:user_id]
 
     # Rate limit the click counts to once in 24 hours
     rate_key = "link-clicks:#{link.id}:#{args[:user_id] || args[:ip]}"
@@ -52,7 +64,7 @@ class TopicLinkClick < ActiveRecord::Base
       create!(topic_link_id: link.id, user_id: args[:user_id], ip_address: args[:ip])
     end
 
-    args[:url]
+    url
   end
 
 end
diff --git a/lib/url_helper.rb b/lib/url_helper.rb
index 19d2fc23d..f34753100 100644
--- a/lib/url_helper.rb
+++ b/lib/url_helper.rb
@@ -18,7 +18,7 @@ module UrlHelper
   end
 
   def schemaless(url)
-    url.gsub(/^https?:/, "")
+    url.sub(/^https?:/, "")
   end
 
 end
diff --git a/spec/controllers/clicks_controller_spec.rb b/spec/controllers/clicks_controller_spec.rb
index c86b1af45..6a1db2d83 100644
--- a/spec/controllers/clicks_controller_spec.rb
+++ b/spec/controllers/clicks_controller_spec.rb
@@ -18,9 +18,7 @@ describe ClicksController do
     context 'correct params' do
       let(:url) { "http://discourse.org" }
 
-      before do
-        request.stubs(:remote_ip).returns('192.168.0.1')
-      end
+      before { request.stubs(:remote_ip).returns('192.168.0.1') }
 
       context "with a made up url" do
         it "doesn't redirect" do
@@ -31,27 +29,15 @@ describe ClicksController do
       end
 
       context "with a query string" do
-        it "tries again without the query if it fails" do
-          TopicLinkClick.expects(:create_from).with(has_entries('url' => 'http://discourse.org/?hello=123')).returns(nil)
-          TopicLinkClick.expects(:create_from).with(has_entries('url' => 'http://discourse.org/')).returns(nil)
+        it "redirects" do
+          TopicLinkClick.expects(:create_from).with(has_entries('url' => 'http://discourse.org/?hello=123')).returns(url)
           xhr :get, :track, url: 'http://discourse.org/?hello=123', post_id: 123
+          expect(response).to redirect_to(url)
         end
       end
 
       context 'with a post_id' do
-        it 'calls create_from' do
-          TopicLinkClick.expects(:create_from).with('url' => url, 'post_id' => '123', 'ip' => '192.168.0.1').returns(url)
-          xhr :get, :track, url: url, post_id: 123
-          expect(response).to redirect_to(url)
-        end
-
-        it 'redirects to the url' do
-          TopicLinkClick.stubs(:create_from).returns(url)
-          xhr :get, :track, url: url, post_id: 123
-          expect(response).to redirect_to(url)
-        end
-
-        it 'will pass the user_id to create_from' do
+        it 'redirects' do
           TopicLinkClick.expects(:create_from).with('url' => url, 'post_id' => '123', 'ip' => '192.168.0.1').returns(url)
           xhr :get, :track, url: url, post_id: 123
           expect(response).to redirect_to(url)
@@ -66,7 +52,7 @@ describe ClicksController do
       end
 
       context 'with a topic_id' do
-        it 'calls create_from' do
+        it 'redirects' do
           TopicLinkClick.expects(:create_from).with('url' => url, 'topic_id' => '789', 'ip' => '192.168.0.1').returns(url)
           xhr :get, :track, url: url, topic_id: 789
           expect(response).to redirect_to(url)