Skip to content

Commit c3c16d8

Browse files
committed
Refactor the image link replacement implementation
Signed-off-by: Ryan Wang <i@ryanc.cc>
1 parent a6069df commit c3c16d8

File tree

5 files changed

+195
-72
lines changed

5 files changed

+195
-72
lines changed

build.gradle

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
plugins {
22
id 'java'
33
id "io.freefair.lombok" version "8.0.1"
4-
id "run.halo.plugin.devtools" version "0.0.5"
4+
id "run.halo.plugin.devtools" version "0.0.9"
55
}
66

77
group 'se.webp.plugin'
@@ -15,7 +15,7 @@ repositories {
1515
}
1616

1717
dependencies {
18-
implementation platform('run.halo.tools.platform:plugin:2.8.0-SNAPSHOT')
18+
implementation platform('run.halo.tools.platform:plugin:2.13.0-SNAPSHOT')
1919
compileOnly 'run.halo.app:api'
2020

2121
testImplementation 'run.halo.app:api'
@@ -24,4 +24,9 @@ dependencies {
2424

2525
test {
2626
useJUnitPlatform()
27+
}
28+
29+
halo {
30+
imageName = "docker.1panel.live/halohub/halo"
31+
version = "2.17"
2732
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package se.webp.plugin;
2+
3+
import lombok.Data;
4+
import reactor.core.publisher.Mono;
5+
import run.halo.app.plugin.ReactiveSettingFetcher;
6+
7+
public class Settings {
8+
9+
public static Mono<BasicConfig> getBasicConfig(ReactiveSettingFetcher settingFetcher) {
10+
return settingFetcher.fetch(BasicConfig.GROUP, BasicConfig.class);
11+
}
12+
13+
@Data
14+
public static class BasicConfig {
15+
public static final String GROUP = "basic";
16+
17+
String proxy_address;
18+
}
19+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package se.webp.plugin;
2+
3+
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;
4+
5+
import java.nio.ByteBuffer;
6+
import java.nio.charset.StandardCharsets;
7+
import java.util.List;
8+
import java.util.Set;
9+
10+
import org.jsoup.Jsoup;
11+
import org.jsoup.nodes.Document;
12+
import org.reactivestreams.Publisher;
13+
import org.springframework.core.io.buffer.DataBuffer;
14+
import org.springframework.core.io.buffer.DataBufferUtils;
15+
import org.springframework.http.HttpMethod;
16+
import org.springframework.http.HttpStatus;
17+
import org.springframework.http.MediaType;
18+
import org.springframework.http.server.reactive.ServerHttpResponse;
19+
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
20+
import org.springframework.lang.NonNull;
21+
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
22+
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
23+
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
24+
import org.springframework.stereotype.Component;
25+
import org.springframework.web.server.ServerWebExchange;
26+
import org.springframework.web.server.WebFilterChain;
27+
28+
import lombok.RequiredArgsConstructor;
29+
import reactor.core.publisher.Flux;
30+
import reactor.core.publisher.Mono;
31+
import run.halo.app.infra.ExternalUrlSupplier;
32+
import run.halo.app.infra.utils.PathUtils;
33+
import run.halo.app.plugin.ReactiveSettingFetcher;
34+
import run.halo.app.security.AdditionalWebFilter;
35+
36+
/**
37+
* This implementation references: https://github.com/guqing/plugin-cloudinary/blob/93f1eb999fa8db5682b13124fa74f0d00efa4d6f/src/main/java/io/github/guqing/cloudinary/DefaultImageOptimizer.java#L19
38+
*/
39+
@Component
40+
@RequiredArgsConstructor
41+
public class WebpCloudImageOptimizerWebFilter implements AdditionalWebFilter {
42+
43+
private final ReactiveSettingFetcher settingFetcher;
44+
45+
private final ExternalUrlSupplier externalUrlSupplier;
46+
47+
private final ServerWebExchangeMatcher pathMatcher = createPathMatcher();
48+
49+
@Override
50+
@NonNull
51+
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
52+
return pathMatcher.matches(exchange)
53+
.flatMap(matchResult -> {
54+
if (matchResult.isMatch() && shouldOptimize(exchange)) {
55+
var decoratedExchange = exchange.mutate()
56+
.response(new ImageOptimizerResponseDecorator(exchange))
57+
.build();
58+
return chain.filter(decoratedExchange);
59+
}
60+
return chain.filter(exchange);
61+
});
62+
}
63+
64+
boolean shouldOptimize(ServerWebExchange exchange) {
65+
var response = exchange.getResponse();
66+
var statusCode = response.getStatusCode();
67+
return statusCode != null && statusCode.isSameCodeAs(HttpStatus.OK);
68+
}
69+
70+
ServerWebExchangeMatcher createPathMatcher() {
71+
var pathMatcher = pathMatchers(HttpMethod.GET, "/**");
72+
var mediaTypeMatcher = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
73+
mediaTypeMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL));
74+
return new AndServerWebExchangeMatcher(pathMatcher, mediaTypeMatcher);
75+
}
76+
77+
class ImageOptimizerResponseDecorator extends ServerHttpResponseDecorator {
78+
79+
public ImageOptimizerResponseDecorator(ServerWebExchange exchange) {
80+
super(exchange.getResponse());
81+
}
82+
83+
boolean isHtmlResponse(ServerHttpResponse response) {
84+
return response.getHeaders().getContentType() != null &&
85+
response.getHeaders().getContentType().includes(MediaType.TEXT_HTML);
86+
}
87+
88+
@Override
89+
@NonNull
90+
public Mono<Void> writeWith(@NonNull Publisher<? extends DataBuffer> body) {
91+
var response = getDelegate();
92+
if (!isHtmlResponse(response)) {
93+
return super.writeWith(body);
94+
}
95+
var bodyWrap = Flux.from(body)
96+
.map(dataBuffer -> {
97+
var byteBuffer = ByteBuffer.allocateDirect(dataBuffer.readableByteCount());
98+
dataBuffer.toByteBuffer(byteBuffer);
99+
DataBufferUtils.release(dataBuffer);
100+
return byteBuffer.asReadOnlyBuffer();
101+
})
102+
.collectSortedList()
103+
.flatMap(byteBuffers -> {
104+
var html = byteBuffersToString(byteBuffers);
105+
106+
return Settings.getBasicConfig(settingFetcher)
107+
.flatMap(config -> {
108+
var proxyAddress = config.getProxy_address();
109+
var optimizedHtml = replaceImageSrc(html, proxyAddress);
110+
var byteBuffer = stringToByteBuffer(optimizedHtml);
111+
return Mono.just(byteBuffer);
112+
});
113+
})
114+
.map(byteBuffer -> response.bufferFactory().wrap(byteBuffer));
115+
return super.writeWith(bodyWrap);
116+
}
117+
118+
private String replaceImageSrc(String html, String proxyAddress) {
119+
Document document = Jsoup.parse(html);
120+
121+
document.select("img").forEach(img -> {
122+
String src = img.attr("src");
123+
124+
if (!PathUtils.isAbsoluteUri(src)) {
125+
img.attr("src", proxyAddress + src);
126+
return;
127+
}
128+
129+
String externalUrl = externalUrlSupplier.get().toString();
130+
131+
if (src.startsWith(externalUrl)) {
132+
img.attr("src",
133+
proxyAddress + src.substring(externalUrl.length()));
134+
}
135+
});
136+
137+
return document.outerHtml();
138+
}
139+
}
140+
141+
private String byteBuffersToString(List<ByteBuffer> byteBuffers) {
142+
int total = byteBuffers.stream().mapToInt(ByteBuffer::remaining).sum();
143+
ByteBuffer combined = ByteBuffer.allocate(total);
144+
145+
for (ByteBuffer buffer : byteBuffers) {
146+
combined.put(buffer);
147+
}
148+
149+
combined.flip();
150+
byte[] byteArray = new byte[combined.remaining()];
151+
combined.get(byteArray);
152+
153+
return new String(byteArray, StandardCharsets.UTF_8);
154+
}
155+
156+
public ByteBuffer stringToByteBuffer(String str) {
157+
byte[] byteArray = str.getBytes(StandardCharsets.UTF_8);
158+
return ByteBuffer.wrap(byteArray);
159+
}
160+
161+
@Override
162+
public int getOrder() {
163+
return LOWEST_PRECEDENCE - 100;
164+
}
165+
}
Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
11
package se.webp.plugin;
22

3-
import org.pf4j.PluginWrapper;
43
import org.springframework.stereotype.Component;
4+
55
import run.halo.app.plugin.BasePlugin;
6+
import run.halo.app.plugin.PluginContext;
67

78
@Component
89
public class WebpCloudPlugin extends BasePlugin {
910

10-
public WebpCloudPlugin(PluginWrapper wrapper) {
11-
super(wrapper);
12-
}
13-
14-
@Override
15-
public void start() {
16-
}
17-
18-
@Override
19-
public void stop() {
11+
public WebpCloudPlugin(PluginContext content) {
12+
super(content);
2013
}
2114
}

src/main/java/se/webp/plugin/WebpCloudPostContentHandler.java

Lines changed: 0 additions & 59 deletions
This file was deleted.

0 commit comments

Comments
 (0)