19 package org.sleuthkit.autopsy.keywordsearch;
21 import com.google.common.collect.Iterators;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.Optional;
28 import java.util.TreeMap;
29 import java.util.logging.Level;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32 import javax.annotation.concurrent.GuardedBy;
33 import org.apache.commons.lang3.StringUtils;
34 import org.apache.solr.client.solrj.SolrQuery;
35 import org.apache.solr.client.solrj.SolrRequest.METHOD;
36 import org.apache.solr.client.solrj.response.QueryResponse;
37 import org.openide.util.NbBundle;
53 class AccountsText
implements IndexedText {
58 private static final String HIGHLIGHT_PRE =
"<span style='background:yellow'>";
59 private static final String ANCHOR_NAME_PREFIX = AccountsText.class.getName() +
"_";
61 private static final String INSERT_PREFIX =
"<a name='" + ANCHOR_NAME_PREFIX;
62 private static final String INSERT_POSTFIX =
"'></a>$0";
63 private static final Pattern ANCHOR_DETECTION_PATTERN = Pattern.compile(HIGHLIGHT_PRE);
65 private static final BlackboardAttribute.Type TSK_KEYWORD_SEARCH_DOCUMENT_ID =
new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_SEARCH_DOCUMENT_ID);
66 private static final BlackboardAttribute.Type TSK_CARD_NUMBER =
new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_CARD_NUMBER);
67 private static final BlackboardAttribute.Type TSK_KEYWORD =
new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD);
69 private static final String FIELD =
Server.
Schema.CONTENT_STR.toString();
73 private final long solrObjectId;
74 private final Collection<? extends BlackboardArtifact> artifacts;
75 private final Set<String> accountNumbers =
new HashSet<>();
76 private final String displayName;
79 private boolean isPageInfoLoaded =
false;
80 private int numberPagesForFile = 0;
81 private Integer currentPage = 0;
86 private final TreeMap<Integer, Integer> numberOfHitsPerPage =
new TreeMap<>();
91 private final Set<Integer> pages = numberOfHitsPerPage.keySet();
95 private final HashMap<Integer, Integer> currentHitPerPage =
new HashMap<>();
97 AccountsText(
long objectID, BlackboardArtifact artifact) {
98 this(objectID, Arrays.asList(artifact));
103 "AccountsText.creditCardNumber=Credit Card Number",
104 "AccountsText.creditCardNumbers=Credit Card Numbers"})
105 AccountsText(
long objectID, Collection<? extends BlackboardArtifact> artifacts) {
106 this.solrObjectId = objectID;
107 this.artifacts = artifacts;
108 displayName = artifacts.size() == 1
109 ? Bundle.AccountsText_creditCardNumber()
110 : Bundle.AccountsText_creditCardNumbers();
114 return this.solrObjectId;
118 public int getNumberPages() {
119 return this.numberPagesForFile;
123 public int getCurrentPage() {
124 return this.currentPage;
128 public boolean hasNextPage() {
129 return getIndexOfCurrentPage() < pages.size() - 1;
134 public boolean hasPreviousPage() {
135 return getIndexOfCurrentPage() > 0;
139 @NbBundle.Messages(
"AccountsText.nextPage.exception.msg=No next page.")
140 public int nextPage() {
142 currentPage = Iterators.get(pages.iterator(), getIndexOfCurrentPage() + 1);
145 throw new IllegalStateException(Bundle.AccountsText_nextPage_exception_msg());
150 @NbBundle.Messages(
"AccountsText.previousPage.exception.msg=No previous page.")
151 public int previousPage() {
152 if (hasPreviousPage()) {
153 currentPage = Iterators.get(pages.iterator(), getIndexOfCurrentPage() - 1);
156 throw new IllegalStateException(Bundle.AccountsText_previousPage_exception_msg());
160 private int getIndexOfCurrentPage() {
161 return Iterators.indexOf(pages.iterator(), this.currentPage::equals);
165 public boolean hasNextItem() {
166 if (this.currentHitPerPage.containsKey(currentPage)) {
167 return this.currentHitPerPage.get(currentPage) < this.numberOfHitsPerPage.get(currentPage);
174 public boolean hasPreviousItem() {
175 if (this.currentHitPerPage.containsKey(currentPage)) {
176 return this.currentHitPerPage.get(currentPage) > 1;
183 @NbBundle.Messages(
"AccountsText.nextItem.exception.msg=No next item.")
184 public int nextItem() {
186 return currentHitPerPage.merge(currentPage, 1, Integer::sum);
188 throw new IllegalStateException(Bundle.AccountsText_nextItem_exception_msg());
193 @NbBundle.Messages(
"AccountsText.previousItem.exception.msg=No previous item.")
194 public int previousItem() {
195 if (hasPreviousItem()) {
196 return currentHitPerPage.merge(currentPage, -1, Integer::sum);
198 throw new IllegalStateException(Bundle.AccountsText_previousItem_exception_msg());
203 public int currentItem() {
204 if (this.currentHitPerPage.containsKey(currentPage)) {
205 return currentHitPerPage.get(currentPage);
216 if (isPageInfoLoaded) {
222 boolean needsQuery =
false;
224 for (BlackboardArtifact artifact : artifacts) {
225 if (solrObjectId != artifact.getObjectID()) {
226 throw new IllegalStateException(
"not all artifacts are from the same object!");
230 this.accountNumbers.add(artifact.getAttribute(TSK_KEYWORD).getValueString());
231 this.accountNumbers.add(artifact.getAttribute(TSK_CARD_NUMBER).getValueString());
234 Optional<Integer> chunkID =
235 Optional.ofNullable(artifact.getAttribute(TSK_KEYWORD_SEARCH_DOCUMENT_ID))
236 .map(BlackboardAttribute::getValueString)
239 .map(Integer::valueOf);
240 if (chunkID.isPresent()) {
241 numberOfHitsPerPage.put(chunkID.get(), 0);
242 currentHitPerPage.put(chunkID.get(), 0);
251 Keyword queryKeyword =
new Keyword(CCN_REGEX,
false,
false);
252 KeywordSearchQuery chunksQuery = KeywordSearchUtil.getQueryForKeyword(queryKeyword,
new KeywordList(Arrays.asList(queryKeyword)));
253 chunksQuery.addFilter(
new KeywordQueryFilter(KeywordQueryFilter.FilterType.CHUNK,
this.solrObjectId));
255 loadPageInfoFromHits(chunksQuery.performQuery());
258 this.currentPage = pages.stream().findFirst().orElse(1);
260 isPageInfoLoaded =
true;
262 private static final String CCN_REGEX =
"(%?)(B?)([0-9][ \\-]*?){12,19}(\\^?)";
267 synchronized private void loadPageInfoFromHits(QueryResults hits) {
269 for (Keyword k : hits.getKeywords()) {
270 for (KeywordHit hit : hits.getResults(k)) {
271 int chunkID = hit.getChunkId();
272 if (chunkID != 0 && this.solrObjectId == hit.getSolrObjectId()) {
273 String hitString = hit.getHit();
274 if (accountNumbers.stream().anyMatch(hitString::contains)) {
275 numberOfHitsPerPage.put(chunkID, 0);
276 currentHitPerPage.put(chunkID, 0);
284 @NbBundle.Messages({
"AccountsText.getMarkup.noMatchMsg="
285 +
"<html><pre><span style\\\\='background\\\\:yellow'>There were no keyword hits on this page. <br />"
286 +
"The keyword could have been in the file name."
287 +
" <br />Advance to another page if present, or to view the original text, choose File Text"
288 +
" <br />in the drop down menu to the right...</span></pre></html>",
289 "AccountsText.getMarkup.queryFailedMsg="
290 +
"<html><pre><span style\\\\='background\\\\:yellow'>Failed to retrieve keyword hit results."
291 +
" <br />Confirm that Autopsy can connect to the Solr server. "
292 +
"<br /></span></pre></html>"})
293 public String getText() {
297 SolrQuery q =
new SolrQuery();
298 q.setShowDebugInfo(DEBUG);
301 final String filterQuery =
Server.
Schema.ID.toString() +
":" + contentIdStr;
303 q.setQuery(filterQuery);
306 QueryResponse queryResponse = solrServer.
query(q, METHOD.POST);
308 String highlightedText =
309 HighlightedText.attemptManualHighlighting(
310 queryResponse.getResults(),
315 highlightedText = insertAnchors(highlightedText);
318 return "<html><pre>" + highlightedText +
"</pre></html>";
319 }
catch (Exception ex) {
320 logger.log(Level.WARNING,
"Error getting highlighted text for " + solrObjectId, ex);
321 return Bundle.AccountsText_getMarkup_queryFailedMsg();
333 private String insertAnchors(String searchableContent) {
338 Matcher m = ANCHOR_DETECTION_PATTERN.matcher(searchableContent);
339 StringBuffer sb =
new StringBuffer(searchableContent.length());
343 m.appendReplacement(sb, INSERT_PREFIX + count + INSERT_POSTFIX);
347 this.numberOfHitsPerPage.put(this.currentPage, count);
348 if (this.currentItem() == 0 && this.hasNextItem()) {
351 return sb.toString();
355 public String toString() {
360 public boolean isSearchable() {
365 public String getAnchorPrefix() {
366 return ANCHOR_NAME_PREFIX;
370 public int getNumberHits() {
371 if (!this.numberOfHitsPerPage.containsKey(
this.currentPage)) {
374 return this.numberOfHitsPerPage.get(this.currentPage);
static Version.Type getBuildType()
static synchronized Server getServer()
static final String CHUNK_ID_SEPARATOR
QueryResponse query(SolrQuery sq)
synchronized static Logger getLogger(String name)
int queryNumFileChunks(long fileID)