mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-02-24 05:18:58 +00:00
(search) Site info view is mostly done
Also optimize the rendering a bit to avoid having to allocate huge string buffers, writing directly to Spark's response instead.
This commit is contained in:
parent
2f4500be5a
commit
7c8a60b8cf
@ -16,6 +16,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation libs.bundles.handlebars
|
implementation libs.bundles.handlebars
|
||||||
implementation libs.guice
|
implementation libs.guice
|
||||||
|
implementation libs.spark
|
||||||
|
|
||||||
testImplementation libs.bundles.slf4j.test
|
testImplementation libs.bundles.slf4j.test
|
||||||
testImplementation libs.bundles.junit
|
testImplementation libs.bundles.junit
|
||||||
|
@ -8,9 +8,12 @@ import lombok.SneakyThrows;
|
|||||||
import nu.marginalia.renderer.config.HandlebarsConfigurator;
|
import nu.marginalia.renderer.config.HandlebarsConfigurator;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import spark.Response;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.Writer;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -48,6 +51,23 @@ public class MustacheRenderer<T> {
|
|||||||
return template.apply(model);
|
return template.apply(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Writer getWriter(Response response) throws IOException {
|
||||||
|
|
||||||
|
// response.raw() has a getWriter() method that fits here, but this is a trap, as subsequent
|
||||||
|
// calls to response.raw().getOutputStream() will fail with an IllegalStateException; and we
|
||||||
|
// have internal code that does this.
|
||||||
|
|
||||||
|
return new OutputStreamWriter(response.raw().getOutputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public Object renderInto(Response response, T model) {
|
||||||
|
|
||||||
|
template.apply(model, getWriter(response));
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public <T2> String render(T model, String name, List<T2> children) {
|
public <T2> String render(T model, String name, List<T2> children) {
|
||||||
Context ctx = Context.newBuilder(model).combine(name, children).build();
|
Context ctx = Context.newBuilder(model).combine(name, children).build();
|
||||||
@ -55,10 +75,22 @@ public class MustacheRenderer<T> {
|
|||||||
return template.apply(ctx);
|
return template.apply(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public <T2> void renderInto(Response response, T model, String name, List<T2> children) {
|
||||||
|
Context ctx = Context.newBuilder(model).combine(name, children).build();
|
||||||
|
|
||||||
|
template.apply(ctx, getWriter(response));
|
||||||
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public <T2> String render(T model, Map<String, ?> children) {
|
public <T2> String render(T model, Map<String, ?> children) {
|
||||||
Context ctx = Context.newBuilder(model).combine(children).build();
|
Context ctx = Context.newBuilder(model).combine(children).build();
|
||||||
return template.apply(ctx);
|
return template.apply(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public <T2> void renderInto(Response response, T model, Map<String, ?> children) {
|
||||||
|
Context ctx = Context.newBuilder(model).combine(children).build();
|
||||||
|
template.apply(ctx, getWriter(response));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,14 @@ public class SearchOperator {
|
|||||||
|
|
||||||
return searchQueryService.getResultsFromQuery(queryResponse);
|
return searchQueryService.getResultsFromQuery(queryResponse);
|
||||||
}
|
}
|
||||||
|
public List<UrlDetails> doBacklinkSearch(Context ctx,
|
||||||
|
String domain) {
|
||||||
|
|
||||||
|
var queryParams = paramFactory.forBacklinkSearch(domain);
|
||||||
|
var queryResponse = queryClient.search(ctx, queryParams);
|
||||||
|
|
||||||
|
return searchQueryService.getResultsFromQuery(queryResponse);
|
||||||
|
}
|
||||||
public DecoratedSearchResults doSearch(Context ctx, SearchParameters userParams) {
|
public DecoratedSearchResults doSearch(Context ctx, SearchParameters userParams) {
|
||||||
|
|
||||||
Future<String> eval = searchUnitConversionService.tryEval(ctx, userParams.query());
|
Future<String> eval = searchUnitConversionService.tryEval(ctx, userParams.query());
|
||||||
|
@ -51,4 +51,22 @@ public class SearchQueryParamFactory {
|
|||||||
SearchSetIdentifier.NONE
|
SearchSetIdentifier.NONE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public QueryParams forBacklinkSearch(String domain) {
|
||||||
|
return new QueryParams("links:"+domain,
|
||||||
|
null,
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
SpecificationLimit.none(),
|
||||||
|
SpecificationLimit.none(),
|
||||||
|
SpecificationLimit.none(),
|
||||||
|
SpecificationLimit.none(),
|
||||||
|
List.of(),
|
||||||
|
new QueryLimits(100, 100, 100, 512),
|
||||||
|
SearchSetIdentifier.NONE
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ public class SearchService extends Service {
|
|||||||
SearchErrorPageService errorPageService,
|
SearchErrorPageService errorPageService,
|
||||||
SearchAddToCrawlQueueService addToCrawlQueueService,
|
SearchAddToCrawlQueueService addToCrawlQueueService,
|
||||||
SearchFlagSiteService flagSiteService,
|
SearchFlagSiteService flagSiteService,
|
||||||
|
SearchSiteInfoService siteInfoService,
|
||||||
SearchQueryService searchQueryService
|
SearchQueryService searchQueryService
|
||||||
) {
|
) {
|
||||||
super(params);
|
super(params);
|
||||||
@ -50,10 +51,10 @@ public class SearchService extends Service {
|
|||||||
|
|
||||||
Spark.post("/public/site/suggest/", addToCrawlQueueService::suggestCrawling);
|
Spark.post("/public/site/suggest/", addToCrawlQueueService::suggestCrawling);
|
||||||
|
|
||||||
Spark.get("/public/site/flag-site/:domainId", flagSiteService::flagSiteForm);
|
|
||||||
Spark.post("/public/site/flag-site/:domainId", flagSiteService::flagSiteAction);
|
|
||||||
Spark.get("/public/site-search/:site/*", this::siteSearchRedir);
|
Spark.get("/public/site-search/:site/*", this::siteSearchRedir);
|
||||||
Spark.get("/public/site/:site", this::siteSearchRedir);
|
|
||||||
|
Spark.get("/public/site/:site", siteInfoService::handle);
|
||||||
|
Spark.post("/public/site/:site", siteInfoService::handlePost);
|
||||||
|
|
||||||
Spark.exception(Exception.class, (e,p,q) -> {
|
Spark.exception(Exception.class, (e,p,q) -> {
|
||||||
logger.error("Error during processing", e);
|
logger.error("Error during processing", e);
|
||||||
|
@ -3,6 +3,7 @@ package nu.marginalia.search.command;
|
|||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import nu.marginalia.search.command.commands.*;
|
import nu.marginalia.search.command.commands.*;
|
||||||
import nu.marginalia.client.Context;
|
import nu.marginalia.client.Context;
|
||||||
|
import spark.Response;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -17,30 +18,32 @@ public class CommandEvaluator {
|
|||||||
BrowseCommand browse,
|
BrowseCommand browse,
|
||||||
ConvertCommand convert,
|
ConvertCommand convert,
|
||||||
DefinitionCommand define,
|
DefinitionCommand define,
|
||||||
SiteListCommand site,
|
|
||||||
BangCommand bang,
|
BangCommand bang,
|
||||||
|
SiteRedirectCommand siteRedirect,
|
||||||
SearchCommand search
|
SearchCommand search
|
||||||
) {
|
) {
|
||||||
specialCommands.add(browse);
|
specialCommands.add(browse);
|
||||||
specialCommands.add(convert);
|
specialCommands.add(convert);
|
||||||
specialCommands.add(define);
|
specialCommands.add(define);
|
||||||
specialCommands.add(site);
|
|
||||||
specialCommands.add(bang);
|
specialCommands.add(bang);
|
||||||
|
specialCommands.add(siteRedirect);
|
||||||
|
|
||||||
defaultCommand = search;
|
defaultCommand = search;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object eval(Context ctx, SearchParameters parameters) {
|
public Object eval(Context ctx, Response response, SearchParameters parameters) {
|
||||||
for (var cmd : specialCommands) {
|
for (var cmd : specialCommands) {
|
||||||
var ret = cmd.process(ctx, parameters);
|
if (cmd.process(ctx, response, parameters)) {
|
||||||
if (ret.isPresent()) {
|
// The commands will write directly to the response, so we don't need to do anything else
|
||||||
return ret.get();
|
// but it's important we don't return null, as this signals to Spark that we haven't handled
|
||||||
|
// the request.
|
||||||
|
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always process the search command last
|
defaultCommand.process(ctx, response, parameters);
|
||||||
return defaultCommand.process(ctx, parameters)
|
return "";
|
||||||
.orElseThrow(() -> new IllegalStateException("Search Command returned Optional.empty()!") /* This Should Not be Possible™ */ );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,8 @@ package nu.marginalia.search.command;
|
|||||||
|
|
||||||
|
|
||||||
import nu.marginalia.client.Context;
|
import nu.marginalia.client.Context;
|
||||||
|
import spark.Response;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface SearchCommandInterface {
|
public interface SearchCommandInterface {
|
||||||
Optional<Object> process(Context ctx, SearchParameters parameters);
|
boolean process(Context ctx, Response response, SearchParameters parameters);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import nu.marginalia.search.command.SearchCommandInterface;
|
|||||||
import nu.marginalia.search.command.SearchParameters;
|
import nu.marginalia.search.command.SearchParameters;
|
||||||
import nu.marginalia.client.Context;
|
import nu.marginalia.client.Context;
|
||||||
import nu.marginalia.search.exceptions.RedirectException;
|
import nu.marginalia.search.exceptions.RedirectException;
|
||||||
|
import spark.Response;
|
||||||
|
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@ -23,7 +24,7 @@ public class BangCommand implements SearchCommandInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Object> process(Context ctx, SearchParameters parameters) {
|
public boolean process(Context ctx, Response response, SearchParameters parameters) {
|
||||||
|
|
||||||
for (var entry : bangsToPattern.entrySet()) {
|
for (var entry : bangsToPattern.entrySet()) {
|
||||||
String bangPattern = entry.getKey();
|
String bangPattern = entry.getKey();
|
||||||
@ -37,7 +38,7 @@ public class BangCommand implements SearchCommandInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Optional.empty();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<String> matchBangPattern(String query, String bangKey) {
|
private Optional<String> matchBangPattern(String query, String bangKey) {
|
||||||
|
@ -17,6 +17,7 @@ import nu.marginalia.renderer.MustacheRenderer;
|
|||||||
import nu.marginalia.renderer.RendererFactory;
|
import nu.marginalia.renderer.RendererFactory;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import spark.Response;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@ -56,16 +57,22 @@ public class BrowseCommand implements SearchCommandInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Object> process(Context ctx, SearchParameters parameters) {
|
public boolean process(Context ctx, Response response, SearchParameters parameters) {
|
||||||
if (!queryPatternPredicate.test(parameters.query())) {
|
if (!queryPatternPredicate.test(parameters.query())) {
|
||||||
return Optional.empty();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Optional.ofNullable(browseSite(ctx, parameters.query()))
|
var model = browseSite(ctx, parameters.query());
|
||||||
.map(results -> browseResultsRenderer.render(results,
|
|
||||||
|
if (null == model)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
browseResultsRenderer.renderInto(response, model,
|
||||||
Map.of("query", parameters.query(),
|
Map.of("query", parameters.query(),
|
||||||
"profile", parameters.profileStr(),
|
"profile", parameters.profileStr(),
|
||||||
"focusDomain", results.focusDomain())));
|
"focusDomain", model.focusDomain())
|
||||||
|
);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
package nu.marginalia.search.command.commands;
|
package nu.marginalia.search.command.commands;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
import nu.marginalia.search.command.SearchCommandInterface;
|
import nu.marginalia.search.command.SearchCommandInterface;
|
||||||
import nu.marginalia.search.command.SearchParameters;
|
import nu.marginalia.search.command.SearchParameters;
|
||||||
import nu.marginalia.search.svc.SearchUnitConversionService;
|
import nu.marginalia.search.svc.SearchUnitConversionService;
|
||||||
import nu.marginalia.client.Context;
|
import nu.marginalia.client.Context;
|
||||||
import nu.marginalia.renderer.MustacheRenderer;
|
import nu.marginalia.renderer.MustacheRenderer;
|
||||||
import nu.marginalia.renderer.RendererFactory;
|
import nu.marginalia.renderer.RendererFactory;
|
||||||
|
import spark.Response;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class ConvertCommand implements SearchCommandInterface {
|
public class ConvertCommand implements SearchCommandInterface {
|
||||||
private final SearchUnitConversionService searchUnitConversionService;
|
private final SearchUnitConversionService searchUnitConversionService;
|
||||||
@ -24,16 +25,19 @@ public class ConvertCommand implements SearchCommandInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Object> process(Context ctx, SearchParameters parameters) {
|
@SneakyThrows
|
||||||
|
public boolean process(Context ctx, Response response, SearchParameters parameters) {
|
||||||
var conversion = searchUnitConversionService.tryConversion(ctx, parameters.query());
|
var conversion = searchUnitConversionService.tryConversion(ctx, parameters.query());
|
||||||
if (conversion.isEmpty()) {
|
if (conversion.isEmpty()) {
|
||||||
return Optional.empty();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Optional.of(conversionRenderer.render(Map.of(
|
conversionRenderer.renderInto(response, Map.of(
|
||||||
"query", parameters.query(),
|
"query", parameters.query(),
|
||||||
"result", conversion.get(),
|
"result", conversion.get(),
|
||||||
"profile", parameters.profileStr()))
|
"profile", parameters.profileStr())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,10 +12,10 @@ import nu.marginalia.renderer.MustacheRenderer;
|
|||||||
import nu.marginalia.renderer.RendererFactory;
|
import nu.marginalia.renderer.RendererFactory;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import spark.Response;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@ -38,17 +38,19 @@ public class DefinitionCommand implements SearchCommandInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Object> process(Context ctx, SearchParameters parameters) {
|
public boolean process(Context ctx, Response response, SearchParameters parameters) {
|
||||||
if (!queryPatternPredicate.test(parameters.query())) {
|
if (!queryPatternPredicate.test(parameters.query())) {
|
||||||
return Optional.empty();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var results = lookupDefinition(ctx, parameters.query());
|
var results = lookupDefinition(ctx, parameters.query());
|
||||||
|
|
||||||
return Optional.of(dictionaryRenderer.render(results,
|
dictionaryRenderer.renderInto(response, results,
|
||||||
Map.of("query", parameters.query(),
|
Map.of("query", parameters.query(),
|
||||||
"profile", parameters.profileStr())
|
"profile", parameters.profileStr())
|
||||||
));
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,9 +10,9 @@ import nu.marginalia.search.model.DecoratedSearchResults;
|
|||||||
import nu.marginalia.search.model.UrlDetails;
|
import nu.marginalia.search.model.UrlDetails;
|
||||||
import nu.marginalia.renderer.MustacheRenderer;
|
import nu.marginalia.renderer.MustacheRenderer;
|
||||||
import nu.marginalia.renderer.RendererFactory;
|
import nu.marginalia.renderer.RendererFactory;
|
||||||
|
import spark.Response;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class SearchCommand implements SearchCommandInterface {
|
public class SearchCommand implements SearchCommandInterface {
|
||||||
private final DomainBlacklist blacklist;
|
private final DomainBlacklist blacklist;
|
||||||
@ -32,10 +32,12 @@ public class SearchCommand implements SearchCommandInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Object> process(Context ctx, SearchParameters parameters) {
|
public boolean process(Context ctx, Response response, SearchParameters parameters) {
|
||||||
DecoratedSearchResults results = searchOperator.doSearch(ctx, parameters);
|
DecoratedSearchResults results = searchOperator.doSearch(ctx, parameters);
|
||||||
|
|
||||||
return Optional.of(searchResultsRenderer.render(results));
|
searchResultsRenderer.renderInto(response, results);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isBlacklisted(UrlDetails details) {
|
private boolean isBlacklisted(UrlDetails details) {
|
||||||
|
@ -1,119 +0,0 @@
|
|||||||
package nu.marginalia.search.command.commands;
|
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import nu.marginalia.db.DbDomainQueries;
|
|
||||||
import nu.marginalia.model.EdgeDomain;
|
|
||||||
import nu.marginalia.search.SearchOperator;
|
|
||||||
import nu.marginalia.search.model.UrlDetails;
|
|
||||||
import nu.marginalia.search.command.SearchCommandInterface;
|
|
||||||
import nu.marginalia.search.command.SearchParameters;
|
|
||||||
import nu.marginalia.search.model.DomainInformation;
|
|
||||||
import nu.marginalia.search.model.SearchProfile;
|
|
||||||
import nu.marginalia.search.siteinfo.DomainInformationService;
|
|
||||||
import nu.marginalia.search.svc.SearchQueryIndexService;
|
|
||||||
import nu.marginalia.client.Context;
|
|
||||||
import nu.marginalia.renderer.MustacheRenderer;
|
|
||||||
import nu.marginalia.renderer.RendererFactory;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
public class SiteListCommand implements SearchCommandInterface {
|
|
||||||
private final DbDomainQueries domainQueries;
|
|
||||||
private final DomainInformationService domainInformationService;
|
|
||||||
private final SearchQueryIndexService searchQueryIndexService;
|
|
||||||
private final SearchOperator searchOperator;
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
|
||||||
|
|
||||||
private final MustacheRenderer<DomainInformation> siteInfoRenderer;
|
|
||||||
|
|
||||||
private final Predicate<String> queryPatternPredicate = Pattern.compile("^site:[.A-Za-z\\-0-9]+$").asPredicate();
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public SiteListCommand(
|
|
||||||
DomainInformationService domainInformationService,
|
|
||||||
DbDomainQueries domainQueries,
|
|
||||||
RendererFactory rendererFactory,
|
|
||||||
SearchQueryIndexService searchQueryIndexService, SearchOperator searchOperator)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
this.domainQueries = domainQueries;
|
|
||||||
this.domainInformationService = domainInformationService;
|
|
||||||
|
|
||||||
siteInfoRenderer = rendererFactory.renderer("search/site-info");
|
|
||||||
this.searchQueryIndexService = searchQueryIndexService;
|
|
||||||
this.searchOperator = searchOperator;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Object> process(Context ctx, SearchParameters parameters) {
|
|
||||||
if (!queryPatternPredicate.test(parameters.query())) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
var results = siteInfo(ctx, parameters.query());
|
|
||||||
var domain = results.getDomain();
|
|
||||||
|
|
||||||
List<UrlDetails> resultSet;
|
|
||||||
Path screenshotPath = null;
|
|
||||||
int domainId = -1;
|
|
||||||
if (null != domain) {
|
|
||||||
resultSet = searchOperator.doSiteSearch(ctx, domain.toString());
|
|
||||||
|
|
||||||
var maybeId = domainQueries.tryGetDomainId(domain);
|
|
||||||
if (maybeId.isPresent()) {
|
|
||||||
domainId = maybeId.getAsInt();
|
|
||||||
screenshotPath = Path.of("/screenshot/" + domainId);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
domainId = -1;
|
|
||||||
screenshotPath = Path.of("/screenshot/0");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
resultSet = Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> renderObject = new HashMap<>(10);
|
|
||||||
|
|
||||||
renderObject.put("query", parameters.query());
|
|
||||||
renderObject.put("hideRanking", true);
|
|
||||||
renderObject.put("profile", parameters.profileStr());
|
|
||||||
renderObject.put("results", resultSet);
|
|
||||||
renderObject.put("screenshot", screenshotPath == null ? "" : screenshotPath.toString());
|
|
||||||
renderObject.put("domainId", domainId);
|
|
||||||
renderObject.put("focusDomain", domain);
|
|
||||||
|
|
||||||
return Optional.of(siteInfoRenderer.render(results, renderObject));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private DomainInformation siteInfo(Context ctx, String humanQuery) {
|
|
||||||
String definePrefix = "site:";
|
|
||||||
String word = humanQuery.substring(definePrefix.length()).toLowerCase();
|
|
||||||
|
|
||||||
logger.info("Fetching Site Info: {}", word);
|
|
||||||
|
|
||||||
var results = domainInformationService
|
|
||||||
.domainInfo(word)
|
|
||||||
.orElseGet(() -> unknownSite(word));
|
|
||||||
|
|
||||||
logger.debug("Results = {}", results);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private DomainInformation unknownSite(String url) {
|
|
||||||
return DomainInformation.builder()
|
|
||||||
.domain(new EdgeDomain(url))
|
|
||||||
.suggestForCrawling(true)
|
|
||||||
.unknownDomain(true)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,49 @@
|
|||||||
|
package nu.marginalia.search.command.commands;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import nu.marginalia.client.Context;
|
||||||
|
import nu.marginalia.search.command.SearchCommandInterface;
|
||||||
|
import nu.marginalia.search.command.SearchParameters;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import spark.Response;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class SiteRedirectCommand implements SearchCommandInterface {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||||
|
|
||||||
|
private final Predicate<String> queryPatternPredicate = Pattern.compile("^site:[.A-Za-z\\-0-9]+$").asPredicate();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public SiteRedirectCommand() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
@Override
|
||||||
|
public boolean process(Context ctx, Response response, SearchParameters parameters) {
|
||||||
|
if (!queryPatternPredicate.test(parameters.query())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String definePrefix = "site:";
|
||||||
|
String domain = parameters.query().substring(definePrefix.length()).toLowerCase();
|
||||||
|
|
||||||
|
// Use an HTML redirect here, so we can use relative URLs
|
||||||
|
|
||||||
|
response.raw().getOutputStream().println("""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Redirecting...</title>
|
||||||
|
<meta http-equiv="refresh" content="0; url=/site/%s">
|
||||||
|
""".formatted(domain));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -76,7 +76,7 @@ public class DomainInformationService {
|
|||||||
.linkingDomains(linkingDomains)
|
.linkingDomains(linkingDomains)
|
||||||
.inCrawlQueue(inCrawlQueue)
|
.inCrawlQueue(inCrawlQueue)
|
||||||
.nodeAffinity(nodeAffinity)
|
.nodeAffinity(nodeAffinity)
|
||||||
.suggestForCrawling((pagesVisited == 0 && !inCrawlQueue))
|
.suggestForCrawling((pagesVisited == 0 && outboundLinks == 0 && !inCrawlQueue))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return Optional.of(di);
|
return Optional.of(di);
|
||||||
|
@ -2,13 +2,7 @@ package nu.marginalia.search.svc;
|
|||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.zaxxer.hikari.HikariDataSource;
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
import nu.marginalia.renderer.MustacheRenderer;
|
|
||||||
import nu.marginalia.renderer.RendererFactory;
|
|
||||||
import spark.Request;
|
|
||||||
import spark.Response;
|
|
||||||
import spark.Spark;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -21,7 +15,6 @@ import java.util.stream.Collectors;
|
|||||||
* DomainComplaintService in control-service
|
* DomainComplaintService in control-service
|
||||||
*/
|
*/
|
||||||
public class SearchFlagSiteService {
|
public class SearchFlagSiteService {
|
||||||
private final MustacheRenderer<FlagSiteViewModel> formTemplate;
|
|
||||||
private final HikariDataSource dataSource;
|
private final HikariDataSource dataSource;
|
||||||
|
|
||||||
private final CategoryItem unknownCategory = new CategoryItem("unknown", "Unknown");
|
private final CategoryItem unknownCategory = new CategoryItem("unknown", "Unknown");
|
||||||
@ -39,61 +32,20 @@ public class SearchFlagSiteService {
|
|||||||
private final Map<String, CategoryItem> categoryItemMap =
|
private final Map<String, CategoryItem> categoryItemMap =
|
||||||
categories.stream().collect(Collectors.toMap(CategoryItem::categoryName, Function.identity()));
|
categories.stream().collect(Collectors.toMap(CategoryItem::categoryName, Function.identity()));
|
||||||
@Inject
|
@Inject
|
||||||
public SearchFlagSiteService(RendererFactory rendererFactory,
|
public SearchFlagSiteService(HikariDataSource dataSource) {
|
||||||
HikariDataSource dataSource) throws IOException {
|
|
||||||
formTemplate = rendererFactory.renderer("search/indict/indict-form");
|
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object flagSiteForm(Request request, Response response) throws SQLException {
|
public List<CategoryItem> getCategories() {
|
||||||
final int domainId = Integer.parseInt(request.params("domainId"));
|
return categories;
|
||||||
|
|
||||||
var model = getModel(domainId, false);
|
|
||||||
return formTemplate.render(model);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object flagSiteAction(Request request, Response response) throws SQLException {
|
public List<FlagSiteComplaintModel> getExistingComplaints(int id) throws SQLException {
|
||||||
|
|
||||||
int domainId = Integer.parseInt(request.params("domainId"));
|
|
||||||
|
|
||||||
var formData = new FlagSiteFormData(
|
|
||||||
domainId,
|
|
||||||
request.queryParams("category"),
|
|
||||||
request.queryParams("description"),
|
|
||||||
request.queryParams("samplequery")
|
|
||||||
);
|
|
||||||
|
|
||||||
insertComplaint(formData);
|
|
||||||
|
|
||||||
return formTemplate.render(getModel(domainId, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void insertComplaint(FlagSiteFormData formData) throws SQLException {
|
|
||||||
try (var conn = dataSource.getConnection();
|
|
||||||
var stmt = conn.prepareStatement(
|
|
||||||
"""
|
|
||||||
INSERT INTO DOMAIN_COMPLAINT(DOMAIN_ID, CATEGORY, DESCRIPTION, SAMPLE) VALUES (?, ?, ?, ?)
|
|
||||||
""")) {
|
|
||||||
stmt.setInt(1, formData.domainId);
|
|
||||||
stmt.setString(2, formData.category);
|
|
||||||
stmt.setString(3, formData.description);
|
|
||||||
stmt.setString(4, formData.sampleQuery);
|
|
||||||
stmt.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private FlagSiteViewModel getModel(int id, boolean isSubmitted) throws SQLException {
|
|
||||||
|
|
||||||
|
|
||||||
try (var conn = dataSource.getConnection();
|
try (var conn = dataSource.getConnection();
|
||||||
var complaintsStmt = conn.prepareStatement("""
|
var complaintsStmt = conn.prepareStatement("""
|
||||||
SELECT CATEGORY, FILE_DATE, REVIEWED, DECISION
|
SELECT CATEGORY, FILE_DATE, REVIEWED, DECISION
|
||||||
FROM DOMAIN_COMPLAINT
|
FROM DOMAIN_COMPLAINT
|
||||||
WHERE DOMAIN_ID=?
|
WHERE DOMAIN_ID=?
|
||||||
""");
|
|
||||||
var stmt = conn.prepareStatement(
|
|
||||||
"""
|
|
||||||
SELECT DOMAIN_NAME FROM EC_DOMAIN WHERE EC_DOMAIN.ID=?
|
|
||||||
"""))
|
"""))
|
||||||
{
|
{
|
||||||
List<FlagSiteComplaintModel> complaints = new ArrayList<>();
|
List<FlagSiteComplaintModel> complaints = new ArrayList<>();
|
||||||
@ -109,21 +61,25 @@ public class SearchFlagSiteService {
|
|||||||
rs.getString(4)));
|
rs.getString(4)));
|
||||||
}
|
}
|
||||||
|
|
||||||
stmt.setInt(1, id);
|
return complaints;
|
||||||
rs = stmt.executeQuery();
|
|
||||||
if (!rs.next()) {
|
|
||||||
Spark.halt(404);
|
|
||||||
}
|
}
|
||||||
return new FlagSiteViewModel(id,
|
}
|
||||||
rs.getString(1),
|
|
||||||
categories,
|
public void insertComplaint(FlagSiteFormData formData) throws SQLException {
|
||||||
complaints,
|
try (var conn = dataSource.getConnection();
|
||||||
isSubmitted);
|
var stmt = conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
INSERT INTO DOMAIN_COMPLAINT(DOMAIN_ID, CATEGORY, DESCRIPTION, SAMPLE) VALUES (?, ?, ?, ?)
|
||||||
|
""")) {
|
||||||
|
stmt.setInt(1, formData.domainId);
|
||||||
|
stmt.setString(2, formData.category);
|
||||||
|
stmt.setString(3, formData.description);
|
||||||
|
stmt.setString(4, formData.sampleQuery);
|
||||||
|
stmt.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record CategoryItem(String categoryName, String categoryDesc) {}
|
public record CategoryItem(String categoryName, String categoryDesc) {}
|
||||||
public record FlagSiteViewModel(int domainId, String domain, List<CategoryItem> category, List<FlagSiteComplaintModel> complaints, boolean isSubmitted) {}
|
|
||||||
public record FlagSiteComplaintModel(String category, String submitTime, boolean isReviewed, String decision) {}
|
public record FlagSiteComplaintModel(String category, String submitTime, boolean isReviewed, String decision) {}
|
||||||
public record FlagSiteFormData(int domainId, String category, String description, String sampleQuery) {};
|
public record FlagSiteFormData(int domainId, String category, String description, String sampleQuery) {};
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ public class SearchQueryService {
|
|||||||
final var ctx = Context.fromRequest(request);
|
final var ctx = Context.fromRequest(request);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return searchCommandEvaulator.eval(ctx, parseParameters(request));
|
return searchCommandEvaulator.eval(ctx, response, parseParameters(request));
|
||||||
}
|
}
|
||||||
catch (RedirectException ex) {
|
catch (RedirectException ex) {
|
||||||
response.redirect(ex.newUrl);
|
response.redirect(ex.newUrl);
|
||||||
|
@ -0,0 +1,236 @@
|
|||||||
|
package nu.marginalia.search.svc;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import nu.marginalia.client.Context;
|
||||||
|
import nu.marginalia.db.DbDomainQueries;
|
||||||
|
import nu.marginalia.model.EdgeDomain;
|
||||||
|
import nu.marginalia.renderer.MustacheRenderer;
|
||||||
|
import nu.marginalia.renderer.RendererFactory;
|
||||||
|
import nu.marginalia.search.SearchOperator;
|
||||||
|
import nu.marginalia.search.model.DomainInformation;
|
||||||
|
import nu.marginalia.search.model.UrlDetails;
|
||||||
|
import nu.marginalia.search.siteinfo.DomainInformationService;
|
||||||
|
import nu.marginalia.search.svc.SearchFlagSiteService.FlagSiteFormData;
|
||||||
|
import spark.*;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
|
public class SearchSiteInfoService {
|
||||||
|
|
||||||
|
private final SearchOperator searchOperator;
|
||||||
|
private final DomainInformationService domainInformationService;
|
||||||
|
private final SearchFlagSiteService flagSiteService;
|
||||||
|
private final DbDomainQueries domainQueries;
|
||||||
|
private final MustacheRenderer<Object> renderer;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public SearchSiteInfoService(SearchOperator searchOperator,
|
||||||
|
DomainInformationService domainInformationService,
|
||||||
|
RendererFactory rendererFactory,
|
||||||
|
SearchFlagSiteService flagSiteService,
|
||||||
|
DbDomainQueries domainQueries) throws IOException {
|
||||||
|
this.searchOperator = searchOperator;
|
||||||
|
this.domainInformationService = domainInformationService;
|
||||||
|
this.flagSiteService = flagSiteService;
|
||||||
|
this.domainQueries = domainQueries;
|
||||||
|
|
||||||
|
this.renderer = rendererFactory.renderer("search/site-info/site-info");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object handle(Request request, Response response) throws SQLException {
|
||||||
|
String domainName = request.params("site");
|
||||||
|
String view = request.queryParamOrDefault("view", "info");
|
||||||
|
|
||||||
|
if (null == domainName || domainName.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctx = Context.fromRequest(request);
|
||||||
|
|
||||||
|
var model = switch (view) {
|
||||||
|
case "links" -> listLinks(ctx, domainName);
|
||||||
|
case "docs" -> listDocs(ctx, domainName);
|
||||||
|
case "info" -> siteInfo(ctx, domainName);
|
||||||
|
case "report" -> reportSite(ctx, domainName);
|
||||||
|
default -> siteInfo(ctx, domainName);
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderer.renderInto(response, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object handlePost(Request request, Response response) throws SQLException {
|
||||||
|
String domainName = request.params("site");
|
||||||
|
String view = request.queryParamOrDefault("view", "info");
|
||||||
|
|
||||||
|
if (null == domainName || domainName.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!view.equals("report"))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
final int domainId = domainQueries.getDomainId(new EdgeDomain(domainName));
|
||||||
|
|
||||||
|
FlagSiteFormData formData = new FlagSiteFormData(
|
||||||
|
domainId,
|
||||||
|
request.queryParams("category"),
|
||||||
|
request.queryParams("description"),
|
||||||
|
request.queryParams("sampleQuery")
|
||||||
|
);
|
||||||
|
flagSiteService.insertComplaint(formData);
|
||||||
|
|
||||||
|
var complaints = flagSiteService.getExistingComplaints(domainId);
|
||||||
|
|
||||||
|
var model = new ReportDomain(domainName, domainId, complaints, List.of(), true);
|
||||||
|
|
||||||
|
return renderer.renderInto(response, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object reportSite(Context ctx, String domainName) throws SQLException {
|
||||||
|
int domainId = domainQueries.getDomainId(new EdgeDomain(domainName));
|
||||||
|
var existingComplaints = flagSiteService.getExistingComplaints(domainId);
|
||||||
|
|
||||||
|
return new ReportDomain(domainName,
|
||||||
|
domainId,
|
||||||
|
existingComplaints,
|
||||||
|
flagSiteService.getCategories(),
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SiteInfo siteInfo(Context ctx, String domainName) {
|
||||||
|
OptionalInt id = domainQueries.tryGetDomainId(new EdgeDomain(domainName));
|
||||||
|
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
return new SiteInfo(domainName, -1, null, dummyInformation(domainName));
|
||||||
|
}
|
||||||
|
|
||||||
|
String screenshotPath = "/screenshot/"+id.getAsInt();
|
||||||
|
DomainInformation domainInfo = domainInformationService
|
||||||
|
.domainInfo(domainName)
|
||||||
|
.orElseGet(() -> dummyInformation(domainName));
|
||||||
|
|
||||||
|
return new SiteInfo(domainName, id.getAsInt(), screenshotPath, domainInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DomainInformation dummyInformation(String domainName) {
|
||||||
|
return DomainInformation.builder()
|
||||||
|
.domain(new EdgeDomain(domainName))
|
||||||
|
.suggestForCrawling(true)
|
||||||
|
.unknownDomain(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Backlinks listLinks(Context ctx, String domainName) {
|
||||||
|
return new Backlinks(domainName,
|
||||||
|
domainQueries.tryGetDomainId(new EdgeDomain(domainName)).orElse(-1),
|
||||||
|
searchOperator.doBacklinkSearch(ctx, domainName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Docs listDocs(Context ctx, String domainName) {
|
||||||
|
return new Docs(domainName,
|
||||||
|
domainQueries.tryGetDomainId(new EdgeDomain(domainName)).orElse(-1),
|
||||||
|
searchOperator.doSiteSearch(ctx, domainName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SiteInfo(Map<String, Boolean> view,
|
||||||
|
Map<String, Boolean> domainState,
|
||||||
|
long domainId,
|
||||||
|
String domain,
|
||||||
|
@Nullable String screenshotUrl,
|
||||||
|
DomainInformation domainInformation)
|
||||||
|
{
|
||||||
|
public SiteInfo(String domain,
|
||||||
|
long domainId,
|
||||||
|
@Nullable String screenshotUrl,
|
||||||
|
DomainInformation domainInformation)
|
||||||
|
{
|
||||||
|
this(Map.of("info", true),
|
||||||
|
Map.of(domainInfoState(domainInformation), true),
|
||||||
|
domainId,
|
||||||
|
domain,
|
||||||
|
screenshotUrl,
|
||||||
|
domainInformation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String domainInfoState(DomainInformation info) {
|
||||||
|
if (info.isBlacklisted()) {
|
||||||
|
return "blacklisted";
|
||||||
|
}
|
||||||
|
if (!info.isUnknownDomain() && info.isSuggestForCrawling()) {
|
||||||
|
return "suggestForCrawling";
|
||||||
|
}
|
||||||
|
if (info.isInCrawlQueue()) {
|
||||||
|
return "inCrawlQueue";
|
||||||
|
}
|
||||||
|
if (info.isUnknownDomain()) {
|
||||||
|
return "unknownDomain";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "indexed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String query() { return "site:" + domain; }
|
||||||
|
|
||||||
|
public boolean isKnown() {
|
||||||
|
return domainId > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Docs(Map<String, Boolean> view,
|
||||||
|
String domain,
|
||||||
|
long domainId,
|
||||||
|
List<UrlDetails> results) {
|
||||||
|
public Docs(String domain, long domainId, List<UrlDetails> results) {
|
||||||
|
this(Map.of("docs", true), domain, domainId, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String focusDomain() { return domain; }
|
||||||
|
|
||||||
|
public String query() { return "site:" + domain; }
|
||||||
|
|
||||||
|
public boolean isKnown() {
|
||||||
|
return domainId > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Backlinks(Map<String, Boolean> view, String domain, long domainId, List<UrlDetails> results) {
|
||||||
|
public Backlinks(String domain, long domainId, List<UrlDetails> results) {
|
||||||
|
this(Map.of("links", true), domain, domainId, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String query() { return "links:" + domain; }
|
||||||
|
|
||||||
|
public boolean isKnown() {
|
||||||
|
return domainId > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ReportDomain(
|
||||||
|
Map<String, Boolean> view,
|
||||||
|
String domain,
|
||||||
|
int domainId,
|
||||||
|
List<SearchFlagSiteService.FlagSiteComplaintModel> complaints,
|
||||||
|
List<SearchFlagSiteService.CategoryItem> category,
|
||||||
|
boolean submitted)
|
||||||
|
{
|
||||||
|
public ReportDomain(String domain,
|
||||||
|
int domainId,
|
||||||
|
List<SearchFlagSiteService.FlagSiteComplaintModel> complaints,
|
||||||
|
List<SearchFlagSiteService.CategoryItem> category,
|
||||||
|
boolean submitted) {
|
||||||
|
this(Map.of("report", true), domain, domainId, complaints, category, submitted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String query() { return "site:" + domain; }
|
||||||
|
|
||||||
|
public boolean isKnown() {
|
||||||
|
return domainId > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -25,10 +25,68 @@ body {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#siteinfo-nav {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
@extend .dialog;
|
||||||
|
padding: 0.25ch !important;
|
||||||
|
margin-top: 1.5ch;
|
||||||
|
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 1ch;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline;
|
||||||
|
padding: 1ch;
|
||||||
|
background-color: $highlight-light2;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-unavailable {
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li.current {
|
||||||
|
background-color: $highlight-light;
|
||||||
|
a {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
box-shadow: 0 0 1ch $border-color;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 1ch;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 0.5ch;
|
||||||
|
font-size: 12pt;
|
||||||
|
background-color: $highlight-light;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background-color: $nicotine-dark;
|
background-color: $nicotine-dark;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-bottom: 1px solid $border-color;
|
border: 1px solid #888;
|
||||||
|
box-shadow: 0 0 0.5ch #888;
|
||||||
margin-bottom: 1ch;
|
margin-bottom: 1ch;
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
@ -46,6 +104,7 @@ header {
|
|||||||
rgba(100,255,100,1) 50%,
|
rgba(100,255,100,1) 50%,
|
||||||
rgba(100,100,255,1) 100%);
|
rgba(100,100,255,1) 100%);
|
||||||
color: black;
|
color: black;
|
||||||
|
text-shadow: 0 0 0.25ch #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover, a:focus {
|
a:hover, a:focus {
|
||||||
@ -55,6 +114,119 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#complaint {
|
||||||
|
@extend .dialog;
|
||||||
|
max-width: 60ch;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-top: 2ch;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 10ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#siteinfo {
|
||||||
|
margin-top: 1ch;
|
||||||
|
display: flex;
|
||||||
|
gap: 1ch;
|
||||||
|
flex-grow: 0.5;
|
||||||
|
flex-shrink: 0.5;
|
||||||
|
flex-basis: 10ch 10ch;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-content: stretch;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
#index-info, #link-info {
|
||||||
|
width: 32ch;
|
||||||
|
@extend .dialog;
|
||||||
|
}
|
||||||
|
#screenshot {
|
||||||
|
@extend .dialog;
|
||||||
|
}
|
||||||
|
#screenshot img {
|
||||||
|
width: 30ch;
|
||||||
|
height: 22.5ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.infobox {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 1ch;
|
||||||
|
margin: 1ch;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
box-shadow: 0 0 1ch $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.cards {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-top: 1ch;
|
||||||
|
gap: 2ch;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: 2px #ccc;
|
||||||
|
background-color: #fff;
|
||||||
|
border-left: 1px solid #ecb;
|
||||||
|
border-top: 1px solid #ecb;
|
||||||
|
box-shadow: #0008 0 0 5px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #fff;
|
||||||
|
background-color: $highlight-light;
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 12pt;
|
||||||
|
padding: .5ch .5ch .5ch .5ch;
|
||||||
|
margin: 0 0 0 0;
|
||||||
|
word-break: break-word;
|
||||||
|
font-family: $heading-fonts;
|
||||||
|
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 a {
|
||||||
|
display: block !important;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:focus img {
|
||||||
|
filter: sepia(100%);
|
||||||
|
box-shadow: #444 0px 0px 20px;
|
||||||
|
}
|
||||||
|
a:focus:not(.nofocus) {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
padding-left: 1ch;
|
||||||
|
padding-right: 1ch;
|
||||||
|
overflow: auto;
|
||||||
|
-webkit-hyphens: auto;
|
||||||
|
-moz-hyphens: auto;
|
||||||
|
-ms-hyphens: auto;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 28ch;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
padding-left: 1ch;
|
||||||
|
padding-right: 1ch;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.positions {
|
.positions {
|
||||||
box-shadow: 0 0 2px #888;
|
box-shadow: 0 0 2px #888;
|
||||||
background-color: #e4e4e4;
|
background-color: #e4e4e4;
|
||||||
@ -258,6 +430,8 @@ footer {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
.utils {
|
.utils {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
@ -278,10 +452,7 @@ footer {
|
|||||||
a {
|
a {
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-device-width: 624px) {
|
@media (max-device-width: 624px) {
|
||||||
body[data-has-js="true"] {
|
body[data-has-js="true"] {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Marginalia Search - {{query}}</title>
|
<title>Marginalia Search - {{query}}</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/style-new.css" />
|
<link rel="stylesheet" href="/serp.css" />
|
||||||
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Marginalia">
|
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Marginalia">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Marginalia Search - File complaint against {{domain}}</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/style-new.css" />
|
|
||||||
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Marginalia">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta name="robots" content="noindex" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
{{>search/parts/search-header}}
|
|
||||||
|
|
||||||
<article>
|
|
||||||
{{>search/parts/search-form}}
|
|
||||||
|
|
||||||
<section class="form">
|
|
||||||
|
|
||||||
{{#if isSubmitted}}
|
|
||||||
<h1>Your complaint against {{domain}} has been submitted</h1>
|
|
||||||
<p>The review process is manual and may take a while.</p>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#unless isSubmitted}}
|
|
||||||
<h1>Flag {{domain}} for review</h1>
|
|
||||||
Note, this is not intended to police acceptable thoughts or ideas.
|
|
||||||
<p>
|
|
||||||
That said, offensive content in obvious bad faith is not tolerated, especially when designed
|
|
||||||
to crop up when you didn't go looking for it. How and where it is said is more
|
|
||||||
important than what is said.
|
|
||||||
<p>
|
|
||||||
This form can also be used to appeal unfairly blacklisted sites.
|
|
||||||
<p>
|
|
||||||
|
|
||||||
<form method="POST" action="/site/flag-site/{{domainId}}">
|
|
||||||
<fieldset>
|
|
||||||
<legend>Flag for Review</legend>
|
|
||||||
|
|
||||||
<label for="category">Category</label><br>
|
|
||||||
<select name="category" id="category">
|
|
||||||
{{#each category}} <option value="{{categoryName}}">{{categoryDesc}}</option> {{/each}}
|
|
||||||
</select>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<label for="description">Description</label><br>
|
|
||||||
<textarea type="text" name="description" id="description" rows=4></textarea><br>
|
|
||||||
<br>
|
|
||||||
<label for="samplequery">(Optional) Search Query </label><br>
|
|
||||||
<input type="text" name="samplequery" id="samplequery" length=255 /><br>
|
|
||||||
<br>
|
|
||||||
<br/>
|
|
||||||
<input type="submit" value="File complaint" />
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
<p>
|
|
||||||
Communicating through forms and tables is a bit impersonal,
|
|
||||||
you may also reach a human being through email at <tt>kontakt@marginalia.nu</tt>.
|
|
||||||
{{/unless}}
|
|
||||||
|
|
||||||
{{#if complaints}}
|
|
||||||
<hr>
|
|
||||||
<h2> Complaints against {{domain}} </h2>
|
|
||||||
<table border width=100%>
|
|
||||||
<tr><th>Category</th><th>Submitted</th><th>Reviewed</th></tr>
|
|
||||||
{{#each complaints}}
|
|
||||||
<tr>
|
|
||||||
<td>{{category}}</td>
|
|
||||||
<td>{{submitTime}}</td>
|
|
||||||
<td>{{#if reviewed}}✓{{/if}}</td>
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
|
||||||
</table>
|
|
||||||
{{/if}}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{{>search/parts/search-footer}}
|
|
||||||
</body>
|
|
@ -1,4 +1,4 @@
|
|||||||
<form action="search" method="get">
|
<form action="/search" method="get">
|
||||||
<div id="search-box">
|
<div id="search-box">
|
||||||
<h1>
|
<h1>
|
||||||
Search The Internet
|
Search The Internet
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
<h2>Indexing Information</h2>
|
|
||||||
<div class="description">
|
|
||||||
<br>
|
|
||||||
{{#if blacklisted}}
|
|
||||||
This website is <em>blacklisted</em>. This excludes it from crawling and indexing.
|
|
||||||
|
|
||||||
<p>This is usually because of some form of misbehavior on the webmaster's end.
|
|
||||||
Either annoying search engine spam, or tasteless content bad faith content.
|
|
||||||
|
|
||||||
<p>Occasionally this is done hastily and in error. If you would like the decision
|
|
||||||
reviewed, you may use <a href="/site/flag-site/{{domainId}}">this form</a> to file a report.</tt>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#unless blacklisted}}
|
|
||||||
<fieldset>
|
|
||||||
<legend>Index</legend>
|
|
||||||
State: {{state}}<br/>
|
|
||||||
Node Affinity: {{nodeAffinity}} </br>
|
|
||||||
Pages Known: {{pagesKnown}} <br/>
|
|
||||||
Pages Crawled: {{pagesFetched}} <br/>
|
|
||||||
Pages Indexed: {{pagesIndexed}} <br/>
|
|
||||||
</fieldset>
|
|
||||||
<br/>
|
|
||||||
{{#if inCrawlQueue}}
|
|
||||||
This website is in the queue for crawling.
|
|
||||||
It may take up to a month before it is indexed.
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if suggestForCrawling}}
|
|
||||||
{{#if unknownDomain}}
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<legend>Crawling</legend>
|
|
||||||
This website is not known to the search engine.
|
|
||||||
|
|
||||||
To submit the website for crawling, follow <a
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
href="https://github.com/MarginaliaSearch/submit-site-to-marginalia-search">these instructions</a>.
|
|
||||||
</fieldset>
|
|
||||||
{{/if}}
|
|
||||||
{{#unless unknownDomain}}
|
|
||||||
<form method="POST" action="/site/suggest/">
|
|
||||||
<fieldset>
|
|
||||||
<legend>Crawling</legend>
|
|
||||||
This website is not queued for crawling. If you would like it to be crawled,
|
|
||||||
use the checkbox and button below.<p/>
|
|
||||||
<input type="hidden" name="id" value="{{domainId}}" />
|
|
||||||
<input type="checkbox" id="nomisclick" name="nomisclick" /> <label for="nomisclick"> This is not a mis-click </label>
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
<input type="submit" value="Add {{domain}} to queue" />
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
{{/unless}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if pagesFetched}}
|
|
||||||
<p>
|
|
||||||
If you've found a reason why this website should not be indexed,
|
|
||||||
you may use <a href="/site/flag-site/{{domainId}}">this form</a> to file a report.<p>
|
|
||||||
{{/if}}
|
|
||||||
{{/unless}}
|
|
||||||
</div>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
@ -1,18 +0,0 @@
|
|||||||
<div class="card info">
|
|
||||||
<h2>Links</h2>
|
|
||||||
<div class="description">
|
|
||||||
<br>
|
|
||||||
<fieldset>
|
|
||||||
<legend>Link Graph</legend>
|
|
||||||
Ranking: {{ranking}}%<br/>
|
|
||||||
Incoming Links: {{incomingLinks}} <br/>
|
|
||||||
Outbound Links: {{outboundLinks}} <br/>
|
|
||||||
</fieldset>
|
|
||||||
<br>
|
|
||||||
<fieldset>
|
|
||||||
<legend>Explore</legend>
|
|
||||||
<a href="/links/{{domain.domain}}">Which pages link here?</a><br/>
|
|
||||||
<a href="/explore/{{domain}}">Explore similar domains</a><br/>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -20,6 +20,12 @@
|
|||||||
|
|
||||||
<section class="sidebar-narrow">
|
<section class="sidebar-narrow">
|
||||||
<section id="results" class="sb-left">
|
<section id="results" class="sb-left">
|
||||||
|
{{#if focusDomain}}
|
||||||
|
<div class="infobox">
|
||||||
|
Showing search results from <a href="/site/{{focusDomain}}">{{focusDomain}}</a>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{/if}}
|
||||||
{{#each results}}{{>search/parts/search-result}}{{/each}}
|
{{#each results}}{{>search/parts/search-result}}{{/each}}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Marginalia Search - {{query}}</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/style-new.css" />
|
|
||||||
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Marginalia">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta name="robots" content="noindex" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
{{>search/parts/search-header}}
|
|
||||||
|
|
||||||
<article>
|
|
||||||
{{>search/parts/search-form}}
|
|
||||||
|
|
||||||
<section class="cards">
|
|
||||||
<div class="card">
|
|
||||||
<h2>{{domain}}</h2>
|
|
||||||
<a href="http://{{domain}}/"><img src="{{screenshot}}" alt="Thumbnail image of {{domain}}"/></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card info">
|
|
||||||
|
|
||||||
{{>search/parts/site-info-index}}
|
|
||||||
{{>search/parts/site-info-links}}
|
|
||||||
|
|
||||||
{{#each results}}{{>search/parts/search-result}}{{/each}}
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{{>search/parts/search-footer}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
|||||||
|
<p>This website is <em>blacklisted</em>. This excludes it from crawling and indexing.</p>
|
||||||
|
|
||||||
|
<p>This is usually because of some form of misbehavior on the webmaster's end.
|
||||||
|
Either annoying search engine spam, or tasteless content bad faith content.</p>
|
||||||
|
|
||||||
|
<p>Occasionally this is done hastily and in error. If you would like the decision
|
||||||
|
reviewed, you may use the <a href="?v=report">report form</a> to file an appeal.</tt>
|
||||||
|
</p>
|
@ -0,0 +1,16 @@
|
|||||||
|
<fieldset>
|
||||||
|
<legend>Index</legend>
|
||||||
|
State: {{state}}<br/>
|
||||||
|
Domain ID: {{domainId}} <br/>
|
||||||
|
Node Affinity: {{nodeAffinity}} <br/>
|
||||||
|
Pages Known: {{pagesKnown}} <br/>
|
||||||
|
Pages Crawled: {{pagesFetched}} <br/>
|
||||||
|
Pages Indexed: {{pagesIndexed}} <br/>
|
||||||
|
</fieldset>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
{{#if pagesFetched}}
|
||||||
|
<p>
|
||||||
|
If you've found a reason why this website should not be indexed,
|
||||||
|
you may use <a href="/site/flag-site/{{domainId}}">this form</a> to file a report.<p>
|
||||||
|
{{/if}}
|
@ -0,0 +1,12 @@
|
|||||||
|
<form method="POST" action="/site/suggest/">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Crawling</legend>
|
||||||
|
This website is not queued for crawling. If you would like it to be crawled,
|
||||||
|
use the checkbox and button below.<p/>
|
||||||
|
<input type="hidden" name="id" value="{{domainId}}" />
|
||||||
|
<input type="checkbox" id="nomisclick" name="nomisclick" /> <label for="nomisclick"> This is not a mis-click </label>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<input type="submit" value="Add {{domain}} to queue" />
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
@ -0,0 +1,9 @@
|
|||||||
|
<fieldset>
|
||||||
|
<legend>Crawling</legend>
|
||||||
|
This website is not known to the search engine.
|
||||||
|
|
||||||
|
To submit the website for crawling, follow <a
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/MarginaliaSearch/submit-site-to-marginalia-search">these instructions</a>.
|
||||||
|
</fieldset>
|
@ -0,0 +1,25 @@
|
|||||||
|
<section id="index-info">
|
||||||
|
<h2>Indexing Information</h2>
|
||||||
|
{{#if domainState.blacklisted}}
|
||||||
|
{{>search/site-info/site-info-index-blacklisted}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if domainState.unknownDomain}}
|
||||||
|
{{>search/site-info/site-info-index-unknown}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if domainState.inCrawlQueue}}
|
||||||
|
<p>
|
||||||
|
This website is in the queue for crawling.
|
||||||
|
It may take up to a month before it is indexed.
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if domainState.suggestForCrawling}}
|
||||||
|
{{>search/site-info/site-info-index-suggest}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if domainState.indexed}}
|
||||||
|
{{>search/site-info/site-info-index-indexed}}
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
@ -0,0 +1,9 @@
|
|||||||
|
<section id="link-info">
|
||||||
|
<h2>Links</h2>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Link Graph</legend>
|
||||||
|
Ranking: {{ranking}}%<br/>
|
||||||
|
Incoming Links: {{incomingLinks}} <br/>
|
||||||
|
Outbound Links: {{outboundLinks}} <br/>
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
@ -0,0 +1,60 @@
|
|||||||
|
<section id="complaint">
|
||||||
|
{{#if submitted}}
|
||||||
|
<h2>Your complaint against {{domain}} has been submitted</h2>
|
||||||
|
<p>The review process is manual and may take a while. If urgent action is necessary,
|
||||||
|
reach me at kontakt@marginalia.nu!
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#unless submitted}}
|
||||||
|
<h2>Flag {{domain}} for review</h2>
|
||||||
|
<p>
|
||||||
|
Note, this is not intended to police acceptable thoughts or ideas.
|
||||||
|
<p>
|
||||||
|
That said, offensive content in obvious bad faith is not tolerated, especially when designed
|
||||||
|
to crop up when you didn't go looking for it. How and where it is said is more
|
||||||
|
important than what is said.
|
||||||
|
<p>
|
||||||
|
This form can also be used to appeal unfairly blacklisted sites.
|
||||||
|
<p>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Flag for Review</legend>
|
||||||
|
|
||||||
|
<label for="category">Category</label><br>
|
||||||
|
<select name="category" id="category">
|
||||||
|
{{#each category}} <option value="{{categoryName}}">{{categoryDesc}}</option> {{/each}}
|
||||||
|
</select>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<label for="description">Description</label><br>
|
||||||
|
<textarea type="text" name="description" id="description" rows=4></textarea><br>
|
||||||
|
<br>
|
||||||
|
<label for="samplequery">(Optional) Search Query </label><br>
|
||||||
|
<input type="text" name="samplequery" id="samplequery" length=255 /><br>
|
||||||
|
<br>
|
||||||
|
<br/>
|
||||||
|
<input type="submit" value="File complaint" />
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
<p>
|
||||||
|
Communicating through forms and tables is a bit impersonal,
|
||||||
|
you may also reach a human being through email at <tt>kontakt@marginalia.nu</tt>.
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
{{#if complaints}}
|
||||||
|
<hr>
|
||||||
|
<h2> Complaints against {{domain}} </h2>
|
||||||
|
<table border width=100%>
|
||||||
|
<tr><th>Category</th><th>Submitted</th><th>Reviewed</th></tr>
|
||||||
|
{{#each complaints}}
|
||||||
|
<tr>
|
||||||
|
<td>{{category}}</td>
|
||||||
|
<td>{{submitTime}}</td>
|
||||||
|
<td>{{#if reviewed}}✓{{/if}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
@ -0,0 +1,58 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Marginalia Search - {{domain}}</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/serp.css" />
|
||||||
|
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Marginalia">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
{{>search/parts/search-header}}
|
||||||
|
|
||||||
|
{{>search/parts/search-form}}
|
||||||
|
|
||||||
|
{{#with view}}
|
||||||
|
<nav id="siteinfo-nav">
|
||||||
|
<h2>{{domain}}</h2>
|
||||||
|
<ul>
|
||||||
|
<li {{#if info}}class="current"{{/if}}><a href="?view=info">Info</a></li>
|
||||||
|
<li {{#if report}}class="current"{{/if}}>{{#if known}}<a href="?view=report">Report</a>{{/if}}{{#unless known}}<a class="link-unavailable" title="This domain is not known by the search engine">Report</a>{{/unless}}</li>
|
||||||
|
<li {{#if docs}}class="current"{{/if}}>{{#if known}}<a href="?view=docs">Docs</a>{{/if}}{{#unless known}}<a class="link-unavailable" title="This domain is not known by the search engine">Docs</a>{{/unless}}</li>
|
||||||
|
<li {{#if links}}class="current"{{/if}}><a href="?view=links">Links</a></li>
|
||||||
|
<li {{#if browse}}class="current"{{/if}}><a href="?view=similar">Similar</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{{/with}}
|
||||||
|
|
||||||
|
{{#if view.info}}{{#with domainInformation}}
|
||||||
|
<section id="siteinfo">
|
||||||
|
{{> search/site-info/site-info-screenshot}}
|
||||||
|
{{> search/site-info/site-info-index}}
|
||||||
|
{{> search/site-info/site-info-links}}
|
||||||
|
</section>
|
||||||
|
{{/with}}{{/if}}
|
||||||
|
|
||||||
|
{{#if view.links}}
|
||||||
|
<div class="infobox">
|
||||||
|
Showing search results with links to {{domain}}.
|
||||||
|
</div>
|
||||||
|
{{#each results}}{{>search/parts/search-result}}{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if view.docs}}
|
||||||
|
<div class="infobox">
|
||||||
|
Showing documents found in {{domain}}.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#each results}}{{>search/parts/search-result}}{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if view.report}}
|
||||||
|
{{>search/site-info/site-info-report}}
|
||||||
|
{{/if}}
|
||||||
|
{{>search/parts/search-footer}}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
|||||||
package nu.marginalia.search.command.commands;
|
|
||||||
|
|
||||||
import nu.marginalia.search.exceptions.RedirectException;
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
|
||||||
|
|
||||||
class BangCommandTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testBang() {
|
|
||||||
var bc = new BangCommand();
|
|
||||||
|
|
||||||
expectRedirectUrl("https://www.google.com/search?q=search+terms", () -> bc.process(null, null, "search terms !g"));
|
|
||||||
expectNoRedirect(() -> bc.process(null, null, "search terms!g"));
|
|
||||||
expectNoRedirect(() -> bc.process(null, null, "!gsearch terms"));
|
|
||||||
expectRedirectUrl("https://www.google.com/search?q=search+terms", () -> bc.process(null, null, "!g search terms"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void expectNoRedirect(Runnable op) {
|
|
||||||
try {
|
|
||||||
op.run();
|
|
||||||
}
|
|
||||||
catch (RedirectException ex) {
|
|
||||||
fail("Expected no redirection, but got " + ex.newUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void expectRedirectUrl(String expectedUrl, Runnable op) {
|
|
||||||
try {
|
|
||||||
op.run();
|
|
||||||
fail("Didn't intercept exception");
|
|
||||||
}
|
|
||||||
catch (RedirectException ex) {
|
|
||||||
Assertions.assertEquals(expectedUrl, ex.newUrl, "Unexpected redirect");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user