You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@roller.apache.org by sn...@apache.org on 2026/10/03 21:15:25 UTC
(roller) branch fix-inline-entry-images-6.1.x created (now 8da9237fd)
This is an automated email from the ASF dual-hosted git repository.
snoopdave pushed a change to branch fix-inline-entry-images-6.1.x
in repository https://gitbox.apache.org/repos/asf/roller.git
at 8da9237fd Preserve pasted entry images when publishing
This branch includes the following new commits:
new 8da9237fd Preserve pasted entry images when publishing
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(roller) 01/01: Preserve pasted entry images when publishing
Posted by sn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
snoopdave pushed a commit to branch fix-inline-entry-images-6.1.x
in repository https://gitbox.apache.org/repos/asf/roller.git
commit 8da9237fdbfa1c5f57bba51b9a7db50743171c61
Author: David M. Johnson <sn...@apache.org>
AuthorDate: Sat Oct 3 12:46:12 2026 -0400
Preserve pasted entry images when publishing
---
.../weblogger/ui/struts2/editor/EntryEdit.java | 206 +++++++++++++++++++++
.../roller/weblogger/util/HTMLSanitizer.java | 17 +-
.../roller/weblogger/util/InlineImageData.java | 156 ++++++++++++++++
.../main/resources/ApplicationResources.properties | 3 +
.../roller/weblogger/config/roller.properties | 6 +
app/src/main/webapp/themes/basic/weblog.vm | 2 +-
app/src/main/webapp/themes/basicmobile/weblog.vm | 2 +-
app/src/main/webapp/themes/fauxcoly/weblog.vm | 3 +-
app/src/main/webapp/themes/frontpage/_header.vm | 2 +-
app/src/main/webapp/themes/gaurav/std_head.vm | 2 +-
docs/roller-user-guide.adoc | 10 +
11 files changed, 395 insertions(+), 14 deletions(-)
diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEdit.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEdit.java
index 8fda6fcbd..d8770a3f1 100644
--- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEdit.java
+++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/EntryEdit.java
@@ -18,12 +18,18 @@
package org.apache.roller.weblogger.ui.struts2.editor;
+import java.io.ByteArrayInputStream;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
@@ -31,12 +37,17 @@ import org.apache.commons.logging.LogFactory;
import org.apache.roller.util.DateUtil;
import org.apache.roller.util.RollerConstants;
import org.apache.roller.weblogger.WebloggerException;
+import org.apache.roller.weblogger.business.MediaFileManager;
import org.apache.roller.weblogger.business.WebloggerFactory;
import org.apache.roller.weblogger.business.WeblogEntryManager;
import org.apache.roller.weblogger.business.plugins.PluginManager;
import org.apache.roller.weblogger.business.plugins.entry.WeblogEntryPlugin;
import org.apache.roller.weblogger.business.search.IndexManager;
+import org.apache.roller.weblogger.config.WebloggerConfig;
+import org.apache.roller.weblogger.config.WebloggerRuntimeConfig;
import org.apache.roller.weblogger.pojos.GlobalPermission;
+import org.apache.roller.weblogger.pojos.MediaFile;
+import org.apache.roller.weblogger.pojos.MediaFileDirectory;
import org.apache.roller.weblogger.pojos.WeblogCategory;
import org.apache.roller.weblogger.pojos.WeblogEntry;
import org.apache.roller.weblogger.pojos.WeblogEntry.PubStatus;
@@ -47,7 +58,10 @@ import org.apache.roller.weblogger.ui.core.plugins.UIPluginManager;
import org.apache.roller.weblogger.ui.core.plugins.WeblogEntryEditor;
import org.apache.roller.weblogger.ui.struts2.util.UIAction;
import org.apache.roller.weblogger.util.EnclosureMetadata;
+import org.apache.roller.weblogger.util.InlineImageData;
import org.apache.roller.weblogger.util.MailUtil;
+import org.apache.roller.weblogger.util.RollerMessages;
+import org.apache.roller.weblogger.util.RollerMessages.RollerMessage;
import org.apache.roller.weblogger.util.cache.CacheManager;
import org.apache.struts2.convention.annotation.AllowedMethods;
import org.apache.struts2.interceptor.validation.SkipValidation;
@@ -200,7 +214,69 @@ public final class EntryEdit extends UIAction {
return failedSave();
}
+ String submittedText = getBean().getText();
+ String submittedSummary = getBean().getSummary();
+ List<MediaFile> createdImages = new ArrayList<>();
+ boolean entrySaved = false;
try {
+ Map<String, InlineImageData.Image> images = new HashMap<>();
+ List<InlineImageData.Source> textImages = InlineImageData.findSources(submittedText);
+ List<InlineImageData.Source> summaryImages = InlineImageData.findSources(submittedSummary);
+ boolean keepInline = WebloggerConfig.getBooleanProperty(
+ "weblog.inlineImages.preferInline")
+ || !WebloggerRuntimeConfig.getBooleanProperty("uploads.enabled")
+ || !getActionWeblog().hasUserPermission(
+ getAuthenticatedUser(), WeblogPermission.POST);
+ long maxUploadBytes = 0;
+ if (!keepInline && (!textImages.isEmpty() || !summaryImages.isEmpty())) {
+ maxUploadBytes = (long) (RollerConstants.ONE_MB_IN_BYTES
+ * new BigDecimal(WebloggerRuntimeConfig.getProperty(
+ "uploads.file.maxsize")).doubleValue());
+ }
+ if (!validateInlineImages(textImages, images, keepInline, maxUploadBytes)
+ || !validateInlineImages(summaryImages, images, keepInline,
+ maxUploadBytes)) {
+ return failedSave();
+ }
+ if (!images.isEmpty()) {
+ if (keepInline) {
+ String inlineText = normalizeInlineSources(submittedText,
+ textImages);
+ String inlineSummary = normalizeInlineSources(submittedSummary,
+ summaryImages);
+ if (!inlineFieldFits(inlineText, textImages)
+ || !inlineFieldFits(inlineSummary, summaryImages)) {
+ return failedSave();
+ }
+ getBean().setText(inlineText);
+ getBean().setSummary(inlineSummary);
+ } else {
+ MediaFileManager mediaManager = WebloggerFactory.getWeblogger()
+ .getMediaFileManager();
+ MediaFileDirectory directory = mediaManager
+ .getDefaultMediaFileDirectory(getActionWeblog());
+ if (directory == null) {
+ directory = mediaManager.createDefaultMediaFileDirectory(
+ getActionWeblog());
+ }
+ Map<String, String> mediaUrls = new HashMap<>();
+ getBean().setText(replaceInlineImages(submittedText,
+ textImages, images, mediaUrls, directory,
+ mediaManager, createdImages));
+ if (!hasActionErrors()) {
+ getBean().setSummary(replaceInlineImages(submittedSummary,
+ summaryImages, images, mediaUrls, directory,
+ mediaManager, createdImages));
+ }
+ if (hasActionErrors()) {
+ getBean().setText(submittedText);
+ getBean().setSummary(submittedSummary);
+ removeCreatedImages(mediaManager, createdImages);
+ return failedSave();
+ }
+ }
+ }
+
WeblogEntryManager weblogEntryManager = WebloggerFactory.getWeblogger()
.getWeblogEntryManager();
@@ -264,6 +340,7 @@ public final class EntryEdit extends UIAction {
log.debug("Saving entry");
weblogEntryManager.saveWeblogEntry(weblogEntry);
WebloggerFactory.getWeblogger().flush();
+ entrySaved = true;
// notify search of the new entry
if (weblogEntry.isPublished()) {
@@ -298,12 +375,141 @@ public final class EntryEdit extends UIAction {
} catch (Exception e) {
log.error("Error saving new entry", e);
+ if (!entrySaved) {
+ getBean().setText(submittedText);
+ getBean().setSummary(submittedSummary);
+ removeCreatedImages(WebloggerFactory.getWeblogger()
+ .getMediaFileManager(), createdImages);
+ }
addError("generic.error.check.logs");
}
}
return failedSave();
}
+ private boolean validateInlineImages(List<InlineImageData.Source> sources,
+ Map<String, InlineImageData.Image> images, boolean keepInline,
+ long maxUploadBytes) {
+ for (InlineImageData.Source source : sources) {
+ if (!keepInline && InlineImageData.exceedsUploadLimit(
+ source.getValue(), maxUploadBytes)) {
+ addError("weblogEdit.inlineImageUploadTooLarge");
+ return false;
+ }
+ InlineImageData.Image image = keepInline
+ ? InlineImageData.parse(source.getValue())
+ : InlineImageData.parseForUpload(source.getValue(),
+ maxUploadBytes);
+ if (image == null) {
+ addError("weblogEdit.inlineImageInvalid");
+ return false;
+ }
+ images.put(source.getValue(), image);
+ }
+ return true;
+ }
+
+ private boolean inlineFieldFits(String html, List<InlineImageData.Source> sources) {
+ if (!sources.isEmpty() && html.getBytes(StandardCharsets.UTF_8).length
+ > InlineImageData.MAX_FIELD_BYTES) {
+ addError("weblogEdit.inlineImageTooLarge");
+ return false;
+ }
+ return true;
+ }
+
+ private String normalizeInlineSources(String html,
+ List<InlineImageData.Source> sources) {
+ if (html == null || sources.isEmpty()) {
+ return html;
+ }
+ StringBuilder result = new StringBuilder(html.length());
+ int cursor = 0;
+ for (InlineImageData.Source source : sources) {
+ result.append(html, cursor, source.getStart());
+ result.append("src=\"").append(source.getValue()).append('"');
+ cursor = source.getEnd();
+ }
+ result.append(html, cursor, html.length());
+ return result.toString();
+ }
+
+ private String replaceInlineImages(String html,
+ List<InlineImageData.Source> sources,
+ Map<String, InlineImageData.Image> images,
+ Map<String, String> mediaUrls, MediaFileDirectory directory,
+ MediaFileManager mediaManager, List<MediaFile> createdImages)
+ throws WebloggerException {
+ if (html == null || sources.isEmpty()) {
+ return html;
+ }
+ StringBuilder result = new StringBuilder(html.length());
+ int cursor = 0;
+ for (InlineImageData.Source source : sources) {
+ String url = mediaUrls.get(source.getValue());
+ if (url == null) {
+ InlineImageData.Image image = images.get(source.getValue());
+ String name = "entry-image-" + UUID.randomUUID() + "."
+ + image.getExtension();
+ RollerMessages errors = new RollerMessages();
+ if (!WebloggerFactory.getWeblogger().getFileContentManager()
+ .canSave(getActionWeblog(), name, image.getContentType(),
+ image.getBytes().length, errors)) {
+ addMediaErrors(errors);
+ return html;
+ }
+ MediaFile media = new MediaFile();
+ media.setName(name);
+ media.setWeblog(getActionWeblog());
+ media.setDirectory(directory);
+ media.setLength(image.getBytes().length);
+ media.setContentType(image.getContentType());
+ media.setInputStream(new ByteArrayInputStream(image.getBytes()));
+ mediaManager.createMediaFile(getActionWeblog(), media, errors);
+ if (errors.getErrorCount() > 0) {
+ addMediaErrors(errors);
+ return html;
+ }
+ createdImages.add(media);
+ url = media.getPermalink();
+ mediaUrls.put(source.getValue(), url);
+ }
+ result.append(html, cursor, source.getStart());
+ result.append("src=\"").append(url).append('"');
+ cursor = source.getEnd();
+ }
+ result.append(html, cursor, html.length());
+ return result.toString();
+ }
+
+ private void addMediaErrors(RollerMessages errors) {
+ for (Iterator<RollerMessage> it = errors.getErrors(); it.hasNext();) {
+ RollerMessage message = it.next();
+ String[] args = message.getArgs();
+ addError(message.getKey(), args == null
+ ? Collections.emptyList() : java.util.Arrays.asList(args));
+ }
+ }
+
+ private void removeCreatedImages(MediaFileManager mediaManager,
+ List<MediaFile> createdImages) {
+ for (MediaFile image : createdImages) {
+ try {
+ mediaManager.removeMediaFile(getActionWeblog(), image);
+ } catch (WebloggerException cleanupError) {
+ log.warn("Could not remove an image from a failed entry save", cleanupError);
+ }
+ }
+ if (!createdImages.isEmpty()) {
+ try {
+ WebloggerFactory.getWeblogger().flush();
+ } catch (WebloggerException cleanupError) {
+ log.warn("Could not flush image cleanup after a failed entry save",
+ cleanupError);
+ }
+ }
+ }
+
EnclosureMetadata validateEnclosure() {
if (StringUtils.isEmpty(getBean().getEnclosureURL())) {
return null;
diff --git a/app/src/main/java/org/apache/roller/weblogger/util/HTMLSanitizer.java b/app/src/main/java/org/apache/roller/weblogger/util/HTMLSanitizer.java
index 280e07917..56a8db5e7 100644
--- a/app/src/main/java/org/apache/roller/weblogger/util/HTMLSanitizer.java
+++ b/app/src/main/java/org/apache/roller/weblogger/util/HTMLSanitizer.java
@@ -211,7 +211,8 @@ public class HTMLSanitizer {
} else if (tag.matches("img|embed") && "src".equals(attr)) {
// <img src="......">
String[] customSchemes = {"http", "https"};
- if (new UrlValidator(customSchemes).isValid(val)) {
+ if (new UrlValidator(customSchemes).isValid(val)
+ || ("img".equals(tag) && InlineImageData.parse(val) != null)) {
foundURL = true;
} else {
ret.invalidTags.add(attr + " " + val);
@@ -374,7 +375,7 @@ public class HTMLSanitizer {
private static List<String> tokenize(String html) {
List<String> tokens = new ArrayList<>();
int pos = 0;
- String token = "";
+ StringBuilder token = new StringBuilder();
int len = html.length();
while (pos < len) {
char c = html.charAt(pos);
@@ -385,11 +386,11 @@ public class HTMLSanitizer {
if ("<!--".equals(ahead)) {
//store the current token
if (token.length() > 0) {
- tokens.add(token);
+ tokens.add(token.toString());
}
//clear the token
- token = "";
+ token.setLength(0);
// search the end of <......>
int end = moveToMarkerEnd(pos, "-->", html);
@@ -402,11 +403,11 @@ public class HTMLSanitizer {
//store the current token
if (token.length() > 0) {
- tokens.add(token);
+ tokens.add(token.toString());
}
//clear the token
- token = "";
+ token.setLength(0);
// serch the end of <......>
int end = moveToMarkerEnd(pos, ">", html);
@@ -414,7 +415,7 @@ public class HTMLSanitizer {
pos = end;
} else {
- token = token + c;
+ token.append(c);
pos++;
}
@@ -422,7 +423,7 @@ public class HTMLSanitizer {
//store the last token
if (token.length() > 0) {
- tokens.add(token);
+ tokens.add(token.toString());
}
return tokens;
diff --git a/app/src/main/java/org/apache/roller/weblogger/util/InlineImageData.java b/app/src/main/java/org/apache/roller/weblogger/util/InlineImageData.java
new file mode 100644
index 000000000..c4c463efa
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/util/InlineImageData.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.roller.weblogger.util;
+
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Image data URLs accepted in entry content and their locations in HTML. */
+public final class InlineImageData {
+
+ // MySQL's TEXT column holds 65,535 bytes. Leave room for the rest of an entry.
+ public static final int MAX_FIELD_BYTES = 60000;
+
+ private static final Pattern IMAGE_TAG = Pattern.compile("(?is)<img\\b[^>]*>");
+ private static final Pattern SOURCE_ATTRIBUTE = Pattern.compile(
+ "(?is)(?<![\\w:-])src\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+))");
+ private static final Pattern DATA_URL = Pattern.compile(
+ "(?i)^data:image/(png|jpeg|gif);base64,([a-z0-9+/]+={0,2})$");
+
+ private InlineImageData() {
+ }
+
+ public static List<Source> findSources(String html) {
+ List<Source> sources = new ArrayList<>();
+ if (html == null) {
+ return sources;
+ }
+ Matcher image = IMAGE_TAG.matcher(html);
+ while (image.find()) {
+ Matcher source = SOURCE_ATTRIBUTE.matcher(image.group());
+ if (!source.find()) {
+ continue;
+ }
+ int group = source.start(1) >= 0 ? 1 : source.start(2) >= 0 ? 2 : 3;
+ String value = source.group(group);
+ if (value.trim().toLowerCase(Locale.ROOT).startsWith("data:")) {
+ sources.add(new Source(image.start() + source.start(),
+ image.start() + source.end(), value));
+ }
+ }
+ return sources;
+ }
+
+ /** Returns null for unsupported, malformed, or oversized image data. */
+ public static Image parse(String value) {
+ return parse(value, true);
+ }
+
+ /** Entry saves may upload larger images under the configured media limit. */
+ public static Image parseForUpload(String value, long maxBytes) {
+ if (exceedsUploadLimit(value, maxBytes)) {
+ return null;
+ }
+ Image image = parse(value, false);
+ return image != null && image.bytes.length <= maxBytes ? image : null;
+ }
+
+ public static boolean exceedsUploadLimit(String value, long maxBytes) {
+ if (value == null || maxBytes < 0) {
+ return true;
+ }
+ // Base64 expands three bytes to four characters; the prefix is short.
+ return value.length() > 64 + ((maxBytes + 2) / 3) * 4;
+ }
+
+ private static Image parse(String value, boolean inline) {
+ if (value == null || (inline && value.length() > MAX_FIELD_BYTES)) {
+ return null;
+ }
+ Matcher match = DATA_URL.matcher(value);
+ if (!match.matches()) {
+ return null;
+ }
+ String type = match.group(1).toLowerCase(Locale.ROOT);
+ try {
+ byte[] bytes = Base64.getDecoder().decode(match.group(2));
+ if (!hasSignature(type, bytes)) {
+ return null;
+ }
+ return new Image(type, bytes);
+ } catch (IllegalArgumentException invalid) {
+ return null;
+ }
+ }
+
+ private static boolean hasSignature(String type, byte[] bytes) {
+ if ("png".equals(type)) {
+ byte[] signature = {(byte) 0x89, 'P', 'N', 'G', 13, 10, 26, 10};
+ if (bytes.length < signature.length) {
+ return false;
+ }
+ for (int i = 0; i < signature.length; i++) {
+ if (bytes[i] != signature[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ if ("jpeg".equals(type)) {
+ return bytes.length >= 3 && bytes[0] == (byte) 0xff
+ && bytes[1] == (byte) 0xd8 && bytes[2] == (byte) 0xff;
+ }
+ return bytes.length >= 6 && bytes[0] == 'G' && bytes[1] == 'I'
+ && bytes[2] == 'F' && bytes[3] == '8'
+ && (bytes[4] == '7' || bytes[4] == '9') && bytes[5] == 'a';
+ }
+
+ public static final class Source {
+ private final int start;
+ private final int end;
+ private final String value;
+
+ private Source(int start, int end, String value) {
+ this.start = start;
+ this.end = end;
+ this.value = value;
+ }
+
+ public int getStart() { return start; }
+ public int getEnd() { return end; }
+ public String getValue() { return value; }
+ }
+
+ public static final class Image {
+ private final String type;
+ private final byte[] bytes;
+
+ private Image(String type, byte[] bytes) {
+ this.type = type;
+ this.bytes = bytes;
+ }
+
+ public String getType() { return type; }
+ public byte[] getBytes() { return bytes; }
+ public String getExtension() { return "jpeg".equals(type) ? "jpg" : type; }
+ public String getContentType() { return "image/" + type; }
+ }
+}
diff --git a/app/src/main/resources/ApplicationResources.properties b/app/src/main/resources/ApplicationResources.properties
index 1aba85d74..c10d10ff3 100644
--- a/app/src/main/resources/ApplicationResources.properties
+++ b/app/src/main/resources/ApplicationResources.properties
@@ -1554,6 +1554,9 @@ weblogEdit.draft=Draft
weblogEdit.draftEntries=Recent Drafts
weblogEdit.deleteEntry=Delete Entry
weblogEdit.insertMediaFile=Insert Media File
+weblogEdit.inlineImageInvalid=This entry contains an unsupported or invalid embedded image. Use a PNG, JPEG or GIF image, or insert a media file.
+weblogEdit.inlineImageTooLarge=This entry has too much embedded image data. Resize the image or ask an administrator to enable media uploads.
+weblogEdit.inlineImageUploadTooLarge=This embedded image exceeds the site's media upload size limit. Resize it before saving.
weblogEdit.fullPreviewMode=Full Preview
weblogEdit.locale=Language
weblogEdit.pendingEntries=Pending Entries
diff --git a/app/src/main/resources/org/apache/roller/weblogger/config/roller.properties b/app/src/main/resources/org/apache/roller/weblogger/config/roller.properties
index cdb7d5252..1e30283c1 100644
--- a/app/src/main/resources/org/apache/roller/weblogger/config/roller.properties
+++ b/app/src/main/resources/org/apache/roller/weblogger/config/roller.properties
@@ -336,6 +336,12 @@ securelogin.enabled=false
# With this settings, all users will have HTML posts sanitized.
weblogAdminsUntrusted=true
+# Upload pasted PNG, JPEG and GIF entry images as media when possible.
+# Set true to retain validated images inline even when uploads are available.
+# Inline images are used automatically if uploads are disabled or the author
+# cannot upload media. Inline entry fields are limited to 60,000 UTF-8 bytes.
+weblog.inlineImages.preferInline=false
+
# Empty value used for passphrase in roller_user table when LDAP or CMA used;
# openid presently generates a random (long) password string instead.
users.passwords.externalAuthValue=<externalAuth>
diff --git a/app/src/main/webapp/themes/basic/weblog.vm b/app/src/main/webapp/themes/basic/weblog.vm
index 17f52ad2d..1652ab7a2 100644
--- a/app/src/main/webapp/themes/basic/weblog.vm
+++ b/app/src/main/webapp/themes/basic/weblog.vm
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src *; font-src 'self'; base-uri 'self'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'">
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src * data:; font-src 'self'; base-uri 'self'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'">
<title>$model.weblog.name</title>
#showAutodiscoveryLinks($model.weblog)
#showAnalyticsTrackingCode($model.weblog)
diff --git a/app/src/main/webapp/themes/basicmobile/weblog.vm b/app/src/main/webapp/themes/basicmobile/weblog.vm
index 88504abad..8cb6de293 100644
--- a/app/src/main/webapp/themes/basicmobile/weblog.vm
+++ b/app/src/main/webapp/themes/basicmobile/weblog.vm
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src *; font-src 'self'; base-uri 'self'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'">
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src * data:; font-src 'self'; base-uri 'self'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'">
<title>$model.weblog.name</title>
#showAutodiscoveryLinks($model.weblog)
#showAnalyticsTrackingCode($model.weblog)
diff --git a/app/src/main/webapp/themes/fauxcoly/weblog.vm b/app/src/main/webapp/themes/fauxcoly/weblog.vm
index bee525959..b8dbd3171 100644
--- a/app/src/main/webapp/themes/fauxcoly/weblog.vm
+++ b/app/src/main/webapp/themes/fauxcoly/weblog.vm
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src *; font-src 'self'; base-uri 'self'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'">
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src * data:; font-src 'self'; base-uri 'self'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'">
#includeTemplate($model.weblog "standard_head")
<title>$model.weblog.name: $model.weblog.tagline</title>
#showAutodiscoveryLinks($model.weblog)
@@ -117,4 +117,3 @@ Click the link below to subscribe via your favorite feed reader:</p>
</div>
</body>
</html>
-
diff --git a/app/src/main/webapp/themes/frontpage/_header.vm b/app/src/main/webapp/themes/frontpage/_header.vm
index 0e4e77005..5c9cdc5bb 100644
--- a/app/src/main/webapp/themes/frontpage/_header.vm
+++ b/app/src/main/webapp/themes/frontpage/_header.vm
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src *; font-src 'self'; base-uri 'self'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'">
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src * data:; font-src 'self'; base-uri 'self'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'">
<title>$model.weblog.name</title>
<link href="$url.absoluteSite/favicon.svg" rel="shortcut icon" type="image/x-icon" />
#showAutodiscoveryLinks($model.weblog)
diff --git a/app/src/main/webapp/themes/gaurav/std_head.vm b/app/src/main/webapp/themes/gaurav/std_head.vm
index 94318dcc7..e68bba6e7 100755
--- a/app/src/main/webapp/themes/gaurav/std_head.vm
+++ b/app/src/main/webapp/themes/gaurav/std_head.vm
@@ -1,5 +1,5 @@
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
-<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src *; font-src 'self'; base-uri 'self'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'">
+<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src * data:; font-src 'self'; base-uri 'self'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'">
#if ($model.permalink == false)
<meta name="Description" content="$model.weblog.about">
#else
diff --git a/docs/roller-user-guide.adoc b/docs/roller-user-guide.adoc
index 050b4ce1a..0a6a0253f 100644
--- a/docs/roller-user-guide.adoc
+++ b/docs/roller-user-guide.adoc
@@ -402,6 +402,16 @@ image::user-guide-11-blogroll.png[]
=== Uploading images and other files to your weblog
+When you paste or drag a local PNG, JPEG or GIF image into the rich text
+editor, Roller stores it as a media file when uploads are available to you.
+If uploads are unavailable, Roller keeps the image in the entry. Inline images
+are limited to 60,000 UTF-8 bytes per content or summary field, including
+the surrounding text. If an older entry contains embedded images, saving it
+again applies the same rule. An administrator can set
+`weblog.inlineImages.preferInline=true` in `roller-custom.properties` to
+prefer inline images even when uploads are available. Custom themes with a
+Content Security Policy must allow `data:` in `img-src` to show inline images.
+
If you’d like to upload images or other files for use in your weblog, go
to your weblog’s *Create & Edit -> Media Files* page. From there you can
upload files, browse and search files. You can also manage your files,