FIX: Preload all associations used for search

FIX: Preload all associations used for search

Implementation of search in encrypted posts uses a client sided cache that contains all possible search results. This data is fetched from /encrypt/posts and it must contain all associations needed for displaying the search results.

The old solution did not account for any customizations, such as plugins that can preload specific information for each search result.

diff --git a/app/controllers/encrypt_controller.rb b/app/controllers/encrypt_controller.rb
index cee0743..2643b3f 100644
--- a/app/controllers/encrypt_controller.rb
+++ b/app/controllers/encrypt_controller.rb
@@ -127,35 +127,6 @@ class DiscourseEncrypt::EncryptController < ApplicationController
     render json: success_json
   end
 
-  # Lists encrypted topics and posts of a user.
-  #
-  # Returns status code 200, topics and posts.
-  def posts
-    posts = Post
-      .includes(topic: [:encrypted_topics_users, :encrypted_topics_data])
-      .where(post_number: 1)
-      .where(encrypted_topics_users: { user_id: current_user.id })
-      .order(created_at: :desc)
-      .limit(1000)
-
-    if SiteSetting.use_pg_headlines_for_excerpt
-      posts = posts.select("'' AS topic_title_headline", posts.arel.projections)
-    end
-
-    if SiteSetting.tagging_enabled && SiteSetting.allow_staff_to_tag_pms
-      posts = posts.includes(topic: :tags)
-    end
-
-    render json: success_json.merge(
-      ActiveModel::ArraySerializer.new(
-        posts,
-        scope: current_user.guardian,
-        each_serializer: SearchPostSerializer,
-        root: :posts
-      ).as_json
-    )
-  end
-
   # Updates an encrypted post, used immediately after creating one to
   # update signature.
   #
@@ -209,6 +180,23 @@ class DiscourseEncrypt::EncryptController < ApplicationController
     render json: success_json.merge(encrypted_pms_count: [pms_count, keys_count].max)
   end
 
+  # Lists encrypted topics and posts of a user to perform client-sided search
+  # in encrypted content.
+  #
+  # Returns status code 200, topics and posts.
+  def posts
+    search = EncryptedSearch.new(
+      'in:first',
+      guardian: guardian,
+      type_filter: 'private_messages',
+      limit: 250,
+    )
+    result = search.execute
+    result.find_user_data(guardian) if result
+
+    render_serialized(result, GroupedSearchResultSerializer, result: result)
+  end
+
   # Get all topic keys for current user.
   #
   # Returns all keys to all encrypted PMs.
diff --git a/lib/encrypted_search.rb b/lib/encrypted_search.rb
new file mode 100644
index 0000000..8b785dd
--- /dev/null
+++ b/lib/encrypted_search.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+# This class is used only to preload all data needed to display encrypted
+# search results. The actual search is performed on the client side.
+class EncryptedSearch < Search
+
+  # Simplified posts_query that does almost nothing, but fetch visible posts.
+  # The term is looked up on the client side.
+  def posts_query(limit, type_filter: nil)
+    Post
+      .includes(topic: :encrypted_topics_data)
+      .where.not(encrypted_topics_data: { title: nil })
+      .where(post_type: Topic.visible_post_types(@guardian.user))
+      .limit(limit)
+  end
+
+  # Similar to posts_query does almost nothing other than to return a set of
+  # posts that might be relevant.
+  def private_messages_search
+    raise Discourse::InvalidAccess.new if @guardian.anonymous?
+
+    @search_pms = true # needed by posts_eager_loads
+    posts = posts_scope(posts_eager_loads(posts_query(@opts[:limit], type_filter: @opts[:type_filter])))
+    posts.each { |post| @results.add(post) }
+  end
+
+  def posts_scope(default_scope = Post.all)
+    if SiteSetting.use_pg_headlines_for_excerpt
+      default_scope.select(
+        "topics.fancy_title AS topic_title_headline",
+        "posts.cooked AS headline",
+        "LEFT(posts.cooked, 50) AS leading_raw_data",
+        "RIGHT(posts.cooked, 50) AS trailing_raw_data",
+        default_scope.arel.projections
+      )
+    else
+      default_scope
+    end
+  end
+end
diff --git a/plugin.rb b/plugin.rb
index eedae6a..ab95e3f 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -31,6 +31,7 @@ after_initialize do
   load File.expand_path('../app/models/user_encryption_key.rb', __FILE__)
   load File.expand_path('../lib/email_sender_extensions.rb', __FILE__)
   load File.expand_path('../lib/encrypted_post_creator.rb', __FILE__)
+  load File.expand_path('../lib/encrypted_search.rb', __FILE__)
   load File.expand_path('../lib/grouped_search_result_serializer_extension.rb', __FILE__)
   load File.expand_path('../lib/openssl.rb', __FILE__)
   load File.expand_path('../lib/post_actions_controller_extensions.rb', __FILE__)
@@ -54,9 +55,9 @@ after_initialize do
     delete '/encrypt/keys'           => 'encrypt#delete_key'
     get    '/encrypt/user'           => 'encrypt#show_user'
     post   '/encrypt/reset'          => 'encrypt#reset_user'
-    get    '/encrypt/posts'          => 'encrypt#posts'
     put    '/encrypt/post'           => 'encrypt#update_post'
     get    '/encrypt/stats'          => 'encrypt#stats'
+    get    '/encrypt/posts'          => 'encrypt#posts'
     get    '/encrypt/rotate'         => 'encrypt#show_all_keys'
     put    '/encrypt/rotate'         => 'encrypt#update_all_keys'
     post   '/encrypt/encrypted_post_timers'  => 'encrypted_post_timers#create'
@@ -84,6 +85,12 @@ after_initialize do
     UserNotificationRenderer.singleton_class.prepend UserNotificationRendererExtensions
   end
 
+  register_search_topic_eager_load do |opts|
+    if SiteSetting.encrypt_enabled? && opts[:search_pms]
+      %i(encrypted_topics_users encrypted_topics_data)
+    end
+  end
+
   # Send plugin-specific topic data to client via serializers.
   #
   # +TopicViewSerializer+ and +BasicTopicSerializer+ should cover all topics
@@ -260,13 +267,6 @@ after_initialize do
   add_permitted_post_create_param(:encrypted_keys)
   add_permitted_post_create_param(:delete_after_minutes)
 
-  # TODO: Remove if check once Discourse 2.6 is stable
-  if respond_to?(:register_search_topic_eager_load)
-    register_search_topic_eager_load do |opts|
-      %i(encrypted_topics_users encrypted_topics_data) if opts[:search_pms] && SiteSetting.encrypt_enabled?
-    end
-  end
-
   NewPostManager.add_handler do |manager|
     next if !manager.args[:encrypted_raw]
 
diff --git a/spec/requests/encrypt_controller_spec.rb b/spec/requests/encrypt_controller_spec.rb
index 09582dc..3ba18cc 100644
--- a/spec/requests/encrypt_controller_spec.rb
+++ b/spec/requests/encrypt_controller_spec.rb
@@ -110,10 +110,35 @@ describe DiscourseEncrypt::EncryptController do
     end
   end
 
+  context '#update_post' do
+    let!(:post) { Fabricate(:encrypt_post) }
+
+    before do
+      SiteSetting.min_trust_to_edit_post = 2
+    end
+
+    it 'is not raising error when user cannot edit because min trust level' do
+      sign_in(post.user)
+      put '/encrypt/post', params: { post_id: post.id, encrypted_raw: 'some encrypted raw' }
+      expect(response.status).to eq(200)
+    end
+
+    it 'does not work if user is not author of post' do
+      sign_in(user)
+      put '/encrypt/post', params: { post_id: post.id, encrypted_raw: 'some encrypted raw' }
+      expect(response.status).to eq(403)
+    end
+  end
+
   context '#posts' do
     let!(:topic) { Fabricate(:encrypt_topic, topic_allowed_users: [ Fabricate.build(:topic_allowed_user, user: user) ]) }
     let!(:post) { Fabricate(:post, topic: topic) }
 
+    before do
+      SearchIndexer.enable
+      SearchIndexer.index(topic, force: true)
+    end
+
     it 'does not work when not logged in' do
       get '/encrypt/posts'
       expect(response.status).to eq(404)
@@ -139,26 +164,6 @@ describe DiscourseEncrypt::EncryptController do
     end
   end
 
-  context '#update_post' do
-    let!(:post) { Fabricate(:encrypt_post) }
-
-    before do
-      SiteSetting.min_trust_to_edit_post = 2
-    end
-
-    it 'is not raising error when user cannot edit because min trust level' do
-      sign_in(post.user)
-      put '/encrypt/post', params: { post_id: post.id, encrypted_raw: 'some encrypted raw' }
-      expect(response.status).to eq(200)
-    end
-
-    it 'does not work if user is not author of post' do
-      sign_in(user)

[... diff too long, it was truncated ...]

GitHub sha: 53845ce30e1023b165f2625671c6da61e437d174

This commit appears in #114 which was approved by ZogStriP. It was merged by udan11.