diff --git a/content.py b/content.py
index f73198d1e..8412964ab 100644
--- a/content.py
+++ b/content.py
@@ -948,7 +948,8 @@ def saveMediaInFormPOST(mediaBytes, debug: bool,
'mp4': 'video/mp4',
'ogv': 'video/ogv',
'mp3': 'audio/mpeg',
- 'ogg': 'audio/ogg'
+ 'ogg': 'audio/ogg',
+ 'zip': 'application/zip'
}
detectedExtension = None
for extension, contentType in extensionList.items():
@@ -960,7 +961,8 @@ def saveMediaInFormPOST(mediaBytes, debug: bool,
extension = 'jpg'
elif extension == 'mpeg':
extension = 'mp3'
- filename = filenameBase + '.' + extension
+ if filenameBase:
+ filename = filenameBase + '.' + extension
attachmentMediaType = \
searchStr.decode().split('/')[0].replace('Content-Type: ', '')
detectedExtension = extension
@@ -979,16 +981,17 @@ def saveMediaInFormPOST(mediaBytes, debug: bool,
break
# remove any existing image files with a different format
- extensionTypes = getImageExtensions()
- for ex in extensionTypes:
- if ex == detectedExtension:
- continue
- possibleOtherFormat = \
- filename.replace('.temp', '').replace('.' +
- detectedExtension, '.' +
- ex)
- if os.path.isfile(possibleOtherFormat):
- os.remove(possibleOtherFormat)
+ if detectedExtension != 'zip':
+ extensionTypes = getImageExtensions()
+ for ex in extensionTypes:
+ if ex == detectedExtension:
+ continue
+ possibleOtherFormat = \
+ filename.replace('.temp', '').replace('.' +
+ detectedExtension, '.' +
+ ex)
+ if os.path.isfile(possibleOtherFormat):
+ os.remove(possibleOtherFormat)
fd = open(filename, 'wb')
if not fd:
diff --git a/daemon.py b/daemon.py
index 60fc2a744..fee03986e 100644
--- a/daemon.py
+++ b/daemon.py
@@ -256,6 +256,7 @@ from cache import checkForChangedActor
from cache import storePersonInCache
from cache import getPersonFromCache
from httpsig import verifyPostHeaders
+from theme import importTheme
from theme import exportTheme
from theme import isNewsThemeName
from theme import getTextModeBanner
@@ -4042,7 +4043,8 @@ class PubServer(BaseHTTPRequestHandler):
profileMediaTypes = ('avatar', 'image',
'banner', 'search_banner',
'instanceLogo',
- 'left_col_image', 'right_col_image')
+ 'left_col_image', 'right_col_image',
+ 'submitImportTheme')
profileMediaTypesUploaded = {}
for mType in profileMediaTypes:
# some images can only be changed by the admin
@@ -4054,18 +4056,18 @@ class PubServer(BaseHTTPRequestHandler):
if debug:
print('DEBUG: profile update extracting ' + mType +
- ' image or font from POST')
+ ' image, zip or font from POST')
mediaBytes, postBytes = \
extractMediaInFormPOST(postBytes, boundary, mType)
if mediaBytes:
if debug:
print('DEBUG: profile update ' + mType +
- ' image or font was found. ' +
+ ' image, zip or font was found. ' +
str(len(mediaBytes)) + ' bytes')
else:
if debug:
print('DEBUG: profile update, no ' + mType +
- ' image or font was found in POST')
+ ' image, zip or font was found in POST')
continue
# Note: a .temp extension is used here so that at no
@@ -4074,6 +4076,13 @@ class PubServer(BaseHTTPRequestHandler):
if mType == 'instanceLogo':
filenameBase = \
baseDir + '/accounts/login.temp'
+ elif mType == 'submitImportTheme':
+ if not os.path.isdir(baseDir + '/imports'):
+ os.mkdir(baseDir + '/imports')
+ filenameBase = \
+ baseDir + '/imports/newtheme.zip'
+ if os.path.isfile(filenameBase):
+ os.remove(filenameBase)
else:
filenameBase = \
baseDir + '/accounts/' + \
@@ -4085,10 +4094,19 @@ class PubServer(BaseHTTPRequestHandler):
filenameBase)
if filename:
print('Profile update POST ' + mType +
- ' media or font filename is ' + filename)
+ ' media, zip or font filename is ' + filename)
else:
print('Profile update, no ' + mType +
- ' media or font filename in POST')
+ ' media, zip or font filename in POST')
+ continue
+
+ if mType == 'submitImportTheme':
+ if nickname == adminNickname or \
+ isArtist(baseDir, nickname):
+ if importTheme(baseDir, filename):
+ print(nickname + ' uploaded a theme')
+ else:
+ print('Only admin or artist can import a theme')
continue
postImageFilename = filename.replace('.temp', '')
@@ -4108,7 +4126,8 @@ class PubServer(BaseHTTPRequestHandler):
filename, postImageFilename, city)
if os.path.isfile(postImageFilename):
print('profile update POST ' + mType +
- ' image or font saved to ' + postImageFilename)
+ ' image, zip or font saved to ' +
+ postImageFilename)
if mType != 'instanceLogo':
lastPartOfImageFilename = \
postImageFilename.split('/')[-1]
diff --git a/theme.py b/theme.py
index 894ee0cc0..42f7c0642 100644
--- a/theme.py
+++ b/theme.py
@@ -10,11 +10,56 @@ import os
from utils import loadJson
from utils import saveJson
from utils import getImageExtensions
+from utils import copytree
from shutil import copyfile
from shutil import make_archive
+from shutil import unpack_archive
from content import dangerousCSS
+def importTheme(baseDir: str, filename: str) -> bool:
+ """Imports a theme
+ """
+ if not os.path.isfile(filename):
+ return False
+ tempThemeDir = baseDir + '/imports/files'
+ if not os.path.isdir(tempThemeDir):
+ os.mkdir(tempThemeDir)
+ unpack_archive(filename, tempThemeDir, 'zip')
+ essentialThemeFiles = ('name.txt', 'theme.json')
+ for themeFile in essentialThemeFiles:
+ if not os.path.isfile(tempThemeDir + '/' + themeFile):
+ print('WARN: ' + themeFile +
+ ' missing from imported theme')
+ return False
+ newThemeName = None
+ with open(tempThemeDir + '/name.txt', 'r') as fp:
+ newThemeName = fp.read().replace('\n', '').replace('\r', '')
+ if len(newThemeName) > 20:
+ print('WARN: Imported theme name is too long')
+ return False
+ if len(newThemeName) < 2:
+ print('WARN: Imported theme name is too short')
+ return False
+ newThemeName = newThemeName.lower()
+ forbiddenChars = (
+ ' ', ';', '/', '\\', '?', '!', '#', '@',
+ ':', '%', '&', '"', '+', '<', '>', '$'
+ )
+ for ch in forbiddenChars:
+ if ch in newThemeName:
+ print('WARN: theme name contains forbidden character')
+ return False
+ if not newThemeName:
+ return False
+ themeDir = baseDir + '/theme/' + newThemeName
+ if not os.path.isdir(themeDir):
+ os.mkdir(themeDir)
+ copytree(tempThemeDir, themeDir)
+ os.remove(tempThemeDir)
+ return os.path.isfile(themeDir + '/theme.json')
+
+
def exportTheme(baseDir: str, theme: str) -> bool:
"""Exports a theme as a zip file
"""
diff --git a/theme/blue/name.txt b/theme/blue/name.txt
new file mode 100644
index 000000000..24560d9b8
--- /dev/null
+++ b/theme/blue/name.txt
@@ -0,0 +1 @@
+blue
diff --git a/theme/debian/name.txt b/theme/debian/name.txt
new file mode 100644
index 000000000..2dee1753e
--- /dev/null
+++ b/theme/debian/name.txt
@@ -0,0 +1 @@
+debian
diff --git a/theme/default/name.txt b/theme/default/name.txt
new file mode 100644
index 000000000..4ad96d515
--- /dev/null
+++ b/theme/default/name.txt
@@ -0,0 +1 @@
+default
diff --git a/theme/hacker/name.txt b/theme/hacker/name.txt
new file mode 100644
index 000000000..52b4d7301
--- /dev/null
+++ b/theme/hacker/name.txt
@@ -0,0 +1 @@
+hacker
diff --git a/theme/henge/name.txt b/theme/henge/name.txt
new file mode 100644
index 000000000..ae57110cf
--- /dev/null
+++ b/theme/henge/name.txt
@@ -0,0 +1 @@
+henge
diff --git a/theme/indymediaclassic/name.txt b/theme/indymediaclassic/name.txt
new file mode 100644
index 000000000..eacded40b
--- /dev/null
+++ b/theme/indymediaclassic/name.txt
@@ -0,0 +1 @@
+indymediaclassic
diff --git a/theme/indymediamodern/name.txt b/theme/indymediamodern/name.txt
new file mode 100644
index 000000000..aaa9e5577
--- /dev/null
+++ b/theme/indymediamodern/name.txt
@@ -0,0 +1 @@
+indymediamodern
diff --git a/theme/lcd/name.txt b/theme/lcd/name.txt
new file mode 100644
index 000000000..b57d509d9
--- /dev/null
+++ b/theme/lcd/name.txt
@@ -0,0 +1 @@
+lcd
diff --git a/theme/light/name.txt b/theme/light/name.txt
new file mode 100644
index 000000000..162faa69f
--- /dev/null
+++ b/theme/light/name.txt
@@ -0,0 +1 @@
+light
diff --git a/theme/night/name.txt b/theme/night/name.txt
new file mode 100644
index 000000000..41feb5b6d
--- /dev/null
+++ b/theme/night/name.txt
@@ -0,0 +1 @@
+night
diff --git a/theme/pixel/name.txt b/theme/pixel/name.txt
new file mode 100644
index 000000000..ae0edb173
--- /dev/null
+++ b/theme/pixel/name.txt
@@ -0,0 +1 @@
+pixel
diff --git a/theme/purple/name.txt b/theme/purple/name.txt
new file mode 100644
index 000000000..08ec89e7f
--- /dev/null
+++ b/theme/purple/name.txt
@@ -0,0 +1 @@
+purple
diff --git a/theme/rc3/name.txt b/theme/rc3/name.txt
new file mode 100644
index 000000000..c215c765c
--- /dev/null
+++ b/theme/rc3/name.txt
@@ -0,0 +1 @@
+rc3
diff --git a/theme/solidaric/name.txt b/theme/solidaric/name.txt
new file mode 100644
index 000000000..5ff20cbee
--- /dev/null
+++ b/theme/solidaric/name.txt
@@ -0,0 +1 @@
+solidaric
diff --git a/theme/starlight/name.txt b/theme/starlight/name.txt
new file mode 100644
index 000000000..8fb456919
--- /dev/null
+++ b/theme/starlight/name.txt
@@ -0,0 +1 @@
+starlight
diff --git a/theme/zen/name.txt b/theme/zen/name.txt
new file mode 100644
index 000000000..d88970a7c
--- /dev/null
+++ b/theme/zen/name.txt
@@ -0,0 +1 @@
+zen
diff --git a/webapp_profile.py b/webapp_profile.py
index 19d8ce03e..ca01cbb9a 100644
--- a/webapp_profile.py
+++ b/webapp_profile.py
@@ -1317,9 +1317,12 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
themesDropdown += '
'
if os.path.isfile(baseDir + '/fonts/custom.woff') or \
os.path.isfile(baseDir + '/fonts/custom.woff2') or \
@@ -1340,7 +1343,7 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
' \n'
graphicsStr += ' \n'
graphicsStr += \
'