main 0.9.0
mr-elbe5 2021-04-01 20:24:11 +02:00
parent 58299d5f68
commit 135e711c4a
133 changed files with 10338 additions and 0 deletions

6
.gitignore vendored 100644
View File

@ -0,0 +1,6 @@
.DS_Store
/.build
/.idea
/Packages
/*.xcodeproj
xcuserdata/

16
Package.resolved 100644
View File

@ -0,0 +1,16 @@
{
"object": {
"pins": [
{
"package": "swift-nio",
"repositoryURL": "https://github.com/apple/swift-nio",
"state": {
"branch": null,
"revision": "3be4e0980075de10a4bc8dee07491d49175cfd7a",
"version": "2.27.0"
}
}
]
},
"version": 1
}

31
Package.swift 100644
View File

@ -0,0 +1,31 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "BandikaSwiftBase",
platforms: [.macOS(.v10_15)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "BandikaSwiftBase",
targets: ["BandikaSwiftBase"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio", from: "2.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "BandikaSwiftBase",
dependencies: [
.product(name: "NIO", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
]),
.testTarget(
name: "BandikaSwiftBaseTests",
dependencies: ["BandikaSwiftBase"]),
]
)

11
README.md 100644
View File

@ -0,0 +1,11 @@
# BandikaSwiftBase
This library contains common classes of SwiftyBandika and SwiftyBandikaCL.
SwiftyBandika is a CMS server app on the Apple App Store (thus including a frontend).
SwiftyBandikaCL is the headless (Command Line based) server.
This library is based on SwiftNIO with NIOHTTP1
It uses its own Request and Response classes as well as an own template type which is more request orientated (so not quite as dumb) compared to other template types.

View File

@ -0,0 +1,64 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class ActionQueue{
public static var instance = ActionQueue()
private var regularActions = Array<RegularAction>()
private var actions = Array<QueuedAction>()
private let semaphore = DispatchSemaphore(value: 1)
public var timer: Timer? = nil
private func lock(){
semaphore.wait()
}
private func unlock(){
semaphore.signal()
}
public func start(){
if timer == nil{
stop()
}
timer = Timer.scheduledTimer(timeInterval: 30.0, target: self, selector: #selector(checkActions), userInfo: nil, repeats: true)
}
public func stop(){
timer?.invalidate()
timer = nil
}
public func addRegularAction(_ action: RegularAction) {
regularActions.append(action)
}
public func addAction(_ action: QueuedAction) {
if !actions.contains(action) {
actions.append(action)
}
}
@objc public func checkActions() {
lock()
defer{unlock()}
for action in regularActions{
action.checkNextExecution()
}
while !actions.isEmpty {
let action = actions.removeFirst()
action.execute()
}
}
}

View File

@ -0,0 +1,32 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class CheckDataAction : QueuedAction{
public static var instance = CheckDataAction()
public static func addToQueue(){
ActionQueue.instance.addAction(instance)
}
override public var type : ActionType{
get{
.checkData
}
}
override public func execute() {
print("checking data")
IdService.instance.checkIdChanged()
UserContainer.instance.checkChanged()
ContentContainer.instance.checkChanged()
}
}

View File

@ -0,0 +1,26 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class CleanupAction : RegularAction{
override public var type : ActionType{
get{
.cleanup
}
}
override public var intervalMinutes: Int {
Statics.instance.cleanupInterval
}
override public func execute() {
ContentContainer.instance.cleanupFiles()
}
}

View File

@ -0,0 +1,32 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public enum ActionType{
case none
case cleanup
case checkData
}
public class QueuedAction: Identifiable, Equatable{
public static func == (lhs: QueuedAction, rhs: QueuedAction) -> Bool {
lhs.type == rhs.type
}
public var type : ActionType{
get {
return .none
}
}
public func execute(){
}
}

View File

@ -0,0 +1,47 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class RegularAction : QueuedAction{
public var nextExecution: Date
override init(){
nextExecution = Application.instance.currentTime
super.init()
}
public var intervalMinutes : Int{
get{
return 0
}
}
public var isActive : Bool{
get{
intervalMinutes != 0
}
}
// other methods
public func checkNextExecution() {
if isActive{
let now = Application.instance.currentTime
let next = nextExecution
if now > next {
ActionQueue.instance.addAction(self)
let seconds = intervalMinutes * 60
nextExecution = now + Double(seconds)
}
}
}
}

View File

@ -0,0 +1,48 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class AdminController: Controller {
public static var instance = AdminController()
override public class var type: ControllerType {
get {
.admin
}
}
override public func processRequest(method: String, id: Int?, request: Request) -> Response? {
switch method {
case "openUserAdministration": return openUserAdministration(request: request)
case "openContentAdministration": return openContentAdministration(request: request)
case "clearClipboard": return clearClipboard(request: request)
default:
return nil
}
}
public func openUserAdministration(request: Request) -> Response {
if !SystemZone.hasUserAnySystemRight(user: request.user){ return Response(code: .forbidden)}
return showUserAdministration(request: request)
}
public func openContentAdministration(request: Request) -> Response {
if !SystemZone.hasUserAnySystemRight(user: request.user){ return Response(code: .forbidden)}
return showContentAdministration(request: request)
}
public func clearClipboard(request: Request) -> Response {
Clipboard.instance.removeData(type: .content)
Clipboard.instance.removeData(type: .file)
return showContentAdministration(request: request)
}
}

View File

@ -0,0 +1,52 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class Application{
public static var instance = Application()
public var language : String{
Statics.instance.defaultLocale.languageCode ?? "en"
}
public var currentTime : Date{
get{
Date()
}
}
public func start(){
Paths.initPaths()
Localizer.initialize(languages: ["en","de"])
Application.instance.initializeData()
ActionQueue.instance.addRegularAction(CleanupAction())
ActionQueue.instance.start()
Log.info("Your shutdown link is 'http://\(Configuration.instance.host):\(Configuration.instance.webPort)/shutdown/\(Statics.instance.shutdownCode)'")
HttpServer.instance.start()
}
public func stop(){
HttpServer.instance.stop()
ActionQueue.instance.checkActions()
ActionQueue.instance.stop()
}
public func initializeData(){
IdService.initialize()
Statics.initialize()
Configuration.initialize()
UserContainer.initialize()
ContentContainer.initialize()
TemplateCache.initialize()
StaticFileController.instance.ensureLayout()
}
}

View File

@ -0,0 +1,107 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class Configuration: DataContainer{
public static var instance = Configuration()
public static func initialize(){
Log.info("initializing configuration")
if !Files.fileExists(path: Paths.configFile){
let config = Configuration()
if !config.save(){
Log.error("could not save default configuration")
}
else {
Log.info("created default configuration")
}
}
if let str = Files.readTextFile(path: Paths.configFile){
if let config : Configuration = Configuration.fromJSON(encoded: str){
instance = config
Log.info("loaded app configuration")
}
}
}
public enum CodingKeys: String, CodingKey {
case host
case webPort
case applicationName
case autostart
public func placeholder() -> String{
"[\(self.rawValue.uppercased())]"
}
}
public var host : String
public var webPort : Int
public var applicationName : String
public var autostart = false
private let configSemaphore = DispatchSemaphore(value: 1)
public required init(){
host = "localhost"
webPort = 8080
applicationName = "SwiftyBandika"
super.init()
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
host = try values.decodeIfPresent(String.self, forKey: .host) ?? "localhost"
webPort = try values.decodeIfPresent(Int.self, forKey: .webPort) ?? 0
applicationName = try values.decodeIfPresent(String.self, forKey: .applicationName) ?? "SwiftyBandika"
autostart = try values.decodeIfPresent(Bool.self, forKey: .autostart) ?? false
try super.init(from: decoder)
}
override public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try super.encode(to: encoder)
try container.encode(host, forKey: .host)
try container.encode(webPort, forKey: .webPort)
try container.encode(applicationName, forKey: .applicationName)
try container.encode(autostart, forKey: .autostart)
}
private func lock(){
configSemaphore.wait()
}
private func unlock(){
configSemaphore.signal()
}
override public func checkChanged(){
if (changed) {
if save() {
Log.info("configuration saved")
changed = false;
}
}
}
override public func save() -> Bool{
Log.info("saving configuration")
lock()
defer{unlock()}
let json = toJSON()
if !Files.saveFile(text: json, path: Paths.configFile){
Log.warn("config file could not be saved")
return false
}
return true
}
}

View File

@ -0,0 +1,88 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public struct Paths{
public static let baseDirectory = FileManager.default.currentDirectoryPath
public static var dataDirectory = "."
public static var fileDirectory = "."
public static var tempFileDirectory = "."
public static var templateDirectory = "."
public static var layoutDirectory = "."
public static var backupDirectory = "."
public static var configFile = "."
public static var contentFile = "."
public static var nextIdFile = "."
public static var staticsFile = "."
public static var usersFile = "."
public static var logFile = "."
public static var resourceDirectory = baseDirectory
public static var webDirectory = resourceDirectory.appendPath("web")
public static var serverPagesDirectory = resourceDirectory.appendPath("serverPages")
public static var defaultContentDirectory = resourceDirectory.appendPath("defaultContent")
public static var defaultTemplateDirectory = resourceDirectory.appendPath("defaultTemplates")
public static var defaultLayoutDirectory = resourceDirectory.appendPath("defaultLayout")
public static func initPaths(){
dataDirectory = baseDirectory.appendPath("BandikaData")
fileDirectory = dataDirectory.appendPath("files")
tempFileDirectory = fileDirectory.appendPath("tmp")
templateDirectory = dataDirectory.appendPath("templates")
layoutDirectory = dataDirectory.appendPath("layout")
backupDirectory = baseDirectory.appendPath("Backup")
configFile = dataDirectory.appendPath("config.json")
contentFile = dataDirectory.appendPath("content.json")
nextIdFile = dataDirectory.appendPath("next.id")
staticsFile = dataDirectory.appendPath("statics.json")
usersFile = dataDirectory.appendPath("users.json")
logFile = baseDirectory.appendPath("bandika.log")
assertDirectories()
if !Files.fileExists(path: logFile){
_ = Files.saveFile(text: "", path: logFile)
}
print("log file is \(logFile)")
Log.info("base directory is \(baseDirectory)")
Log.info("data directory is \(dataDirectory)")
Log.info("file directory is \(fileDirectory)")
Log.info("template directory is \(templateDirectory)")
Log.info("layout directory is \(layoutDirectory)")
Log.info("resource directory is \(resourceDirectory)")
Log.info("web directory is \(webDirectory)")
}
public static func assertDirectories(){
do {
if !Files.fileExists(path: dataDirectory) {
try FileManager.default.createDirectory(at: dataDirectory.toDirectoryUrl()!, withIntermediateDirectories: true, attributes: nil)
}
if !Files.fileExists(path: fileDirectory) {
try FileManager.default.createDirectory(at: fileDirectory.toDirectoryUrl()!, withIntermediateDirectories: true, attributes: nil)
}
if !Files.fileExists(path: tempFileDirectory) {
try FileManager.default.createDirectory(at: tempFileDirectory.toDirectoryUrl()!, withIntermediateDirectories: true, attributes: nil)
}
if !Files.fileExists(path: templateDirectory) {
try FileManager.default.createDirectory(at: templateDirectory.toDirectoryUrl()!, withIntermediateDirectories: true, attributes: nil)
}
if !Files.fileExists(path: layoutDirectory) {
try FileManager.default.createDirectory(at: layoutDirectory.toDirectoryUrl()!, withIntermediateDirectories: true, attributes: nil)
}
if !Files.fileExists(path: backupDirectory) {
try FileManager.default.createDirectory(at: backupDirectory.toDirectoryUrl()!, withIntermediateDirectories: true, attributes: nil)
}
}
catch{
Log.error("could not create all directories")
}
}
}

View File

@ -0,0 +1,34 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
extension Array{
mutating public func remove<T : Equatable>(obj : T){
for i in 0..<count{
if obj == self[i] as? T{
remove(at: i)
return
}
}
}
public func getTypedArray<T>(type: T.Type) -> Array<T>{
var arr = Array<T>()
for data in self{
if let obj = data as? T {
arr.append(obj)
}
}
return arr
}
}

View File

@ -0,0 +1,58 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
extension Decodable{
public static func deserialize<T: Decodable>(encoded : String) -> T?{
if let data = Data(base64Encoded: encoded){
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try? decoder.decode(T.self, from : data)
}
return nil
}
public static func fromJSON<T: Decodable>(encoded : String) -> T?{
if let data = encoded.data(using: .utf8){
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try? decoder.decode(T.self, from : data)
}
return nil
}
}
extension Encodable{
public func serialize() -> String{
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .iso8601
if let json = try? encoder.encode(self).base64EncodedString(){
return json
}
return ""
}
public func toJSON() -> String{
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .iso8601
if let data = try? encoder.encode(self){
if let s = String(data:data, encoding: .utf8){
return s
}
}
return ""
}
}

View File

@ -0,0 +1,95 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
extension Date{
public func startOfDay() -> Date{
var cal = Calendar.current
cal.timeZone = TimeZone(abbreviation: "UTC")!
return cal.startOfDay(for: self)
}
public func startOfMonth() -> Date{
var cal = Calendar.current
cal.timeZone = TimeZone(abbreviation: "UTC")!
let components = cal .dateComponents([.month, .year], from: self)
return cal.date(from: components)!
}
public func dateString() -> String{
DateFormats.dateOnlyFormatter.string(from: self)
}
public func dateTimeString() -> String{
DateFormats.dateTimeFormatter.string(from: self)
}
public func fileDate() -> String{
DateFormats.fileDateFormatter.string(from: self)
}
public func timeString() -> String{
DateFormats.timeOnlyFormatter.string(from: self)
}
}
extension Date {
public var millisecondsSince1970:Int64 {
Int64((timeIntervalSince1970 * 1000.0).rounded())
}
init(milliseconds:Int) {
self = Date(timeIntervalSince1970: TimeInterval(milliseconds / 1000))
}
}
public class DateFormats{
public static var dateOnlyFormatter : DateFormatter{
get{
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
return dateFormatter
}
}
public static var timeOnlyFormatter : DateFormatter{
get{
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
dateFormatter.timeStyle = .short
return dateFormatter
}
}
public static var dateTimeFormatter : DateFormatter{
get{
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .short
return dateFormatter
}
}
public static var fileDateFormatter : DateFormatter{
get{
let dateFormatter = DateFormatter()
dateFormatter.timeZone = .none
dateFormatter.dateFormat = "yyyyMMddHHmmss"
return dateFormatter
}
}
}

View File

@ -0,0 +1,59 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
extension Dictionary{
mutating public func addAll<K, V>(from src: [K:V]){
for (k, v) in src {
if let key = k as? Key, let val = v as? Value {
self[key] = val
}
}
}
public func getTypedObject<K,T>(key: K, type: T.Type) -> T?{
if let k = key as? Key{
if let val = self[k] as? T{
return val
}
}
return nil
}
public func getTypedValues<T>(type: T.Type) -> Array<T>{
var arr = Array<T>()
for value in values{
if let val = value as? T{
arr.append(val)
}
}
return arr
}
mutating public func remove<T : Equatable>(key : T){
for k in keys{
if key == k as? T{
self[k] = nil
return
}
}
}
mutating public func remove<T : Equatable>(value : T){
for k in keys{
if value == self[k] as? T{
self[k] = nil
return
}
}
}
}

View File

@ -0,0 +1,287 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class Files {
public static func fileExists(path: String) -> Bool {
FileManager.default.fileExists(atPath: path)
}
public static func isDirectory(path: String) -> Bool {
var isDir = ObjCBool(false)
if FileManager.default.fileExists(atPath: path, isDirectory: &isDir){
return isDir.boolValue
}
return false
}
public static func isFile(path: String) -> Bool {
var isDir = ObjCBool(false)
if FileManager.default.fileExists(atPath: path, isDirectory: &isDir){
return !isDir.boolValue
}
return false
}
public static func directoryIsEmpty(path: String) -> Bool {
if let contents = try? FileManager.default.contentsOfDirectory(atPath: path) {
return contents.isEmpty
}
return false
}
public static func createDirectory(path: String) -> Bool {
if let url = path.toDirectoryUrl() {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
return true
} catch {
}
}
return false
}
public static func readFile(path: String) -> Data? {
if let fileData = FileManager.default.contents(atPath: path) {
return fileData
}
return nil
}
public static func readTextFile(path: String) -> String? {
if let url = path.toFileUrl() {
do {
let string = try String(contentsOf: url, encoding: .utf8)
return string
} catch {
}
}
return nil
}
public static func saveFile(data: Data, path: String) -> Bool {
if let url = path.toFileUrl() {
do {
try data.write(to: url, options: .atomic)
return true
} catch let err {
Log.error("Error saving file \(url.path): " + err.localizedDescription)
}
}
return false
}
public static func saveFile(text: String, path: String) -> Bool {
if let url = path.toFileUrl() {
do {
try text.write(to: url, atomically: true, encoding: .utf8)
return true
} catch let err {
Log.error("Error saving file \(url.path): " + err.localizedDescription)
}
}
return false
}
public static func appendToFile(text: String, url: URL){
if let fileHandle = try? FileHandle(forWritingTo: url), let data = text.data(using: .utf8){
fileHandle.seekToEndOfFile()
fileHandle.write(data)
fileHandle.closeFile()
}
}
public static func copyFile(name: String, fromDir: String, toDir: String, replace: Bool = false) -> Bool {
do {
let toPath = toDir.appendPath(name)
if replace && fileExists(path: toPath) {
_ = deleteFile(path: toPath)
}
let fromPath = fromDir.appendPath(name)
try FileManager.default.copyItem(atPath: fromPath, toPath: toPath)
return true
} catch let err {
Log.error("Error copying file \(name): " + err.localizedDescription)
return false
}
}
public static func copyFile(from: String, to: String, replace: Bool = false) -> Bool {
do {
if replace && fileExists(path: to) {
_ = deleteFile(path: to)
}
try FileManager.default.copyItem(atPath: from, toPath: to)
return true
} catch let err {
Log.error("Error copying file \(from): " + err.localizedDescription)
return false
}
}
public static func copyDirectory(from: String, to: String) {
do {
let childNames = try FileManager.default.contentsOfDirectory(atPath: from)
for name in childNames {
try FileManager.default.copyItem(atPath: from.appendPath(name), toPath: to.appendPath(name))
}
} catch let err {
Log.error("Error copying directory content: " + err.localizedDescription)
}
}
public static func moveFile(from: String, to: String, replace: Bool = false) -> Bool {
do {
if replace && fileExists(path: to) {
_ = deleteFile(path: to)
}
try FileManager.default.moveItem(atPath: from, toPath: to)
return true
} catch let err {
Log.error("Error moving file \(from): " + err.localizedDescription)
return false
}
}
public static func renameFile(dir: String, fromName: String, toName: String) -> Bool {
do {
try FileManager.default.moveItem(atPath: dir.appendPath(fromName), toPath: dir.appendPath(toName))
return true
} catch {
return false
}
}
public static func deleteFile(dir: String, fileName: String) -> Bool {
do {
try FileManager.default.removeItem(atPath: dir.appendPath(fileName))
Log.info("file deleted: \(fileName)")
return true
} catch {
return false
}
}
public static func deleteFile(path: String) -> Bool {
do {
try FileManager.default.removeItem(atPath: path)
Log.info("file deleted: \(path)")
return true
} catch {
return false
}
}
public static func listAllFileNames(dirPath: String) -> Array<String> {
if let arr = try? FileManager.default.contentsOfDirectory(atPath: dirPath) {
return arr
}
return Array<String>()
}
public static func listAllDirectories(dirPath: String) -> Array<String> {
var dirs = Array<String>()
for name in listAllFileNames(dirPath: dirPath){
let path = dirPath.appendPath(name)
if isDirectory(path: path){
dirs.append(path)
}
}
return dirs
}
public static func listAllFiles(dirPath: String) -> Array<String> {
var files = Array<String>()
for name in listAllFileNames(dirPath: dirPath){
let path = dirPath.appendPath(name)
if isFile(path: path){
files.append(path)
}
}
return files
}
public static func deleteAllFiles(dir: String, except: Set<String>) -> Bool {
var success = true
let fileNames = listAllFileNames(dirPath: dir)
for name in fileNames {
if !except.contains(name) {
if !deleteFile(dir: dir, fileName: name) {
Log.warn("could not delete file \(name)")
success = false
}
}
}
return success
}
public static func deleteAllFiles(dir: String) {
let fileNames = listAllFileNames(dirPath: dir)
var count = 0
for name in fileNames {
if deleteFile(dir: dir, fileName: name) {
count += 1
}
}
Log.info("\(count) files deleted")
}
public static func getExtension(fileName: String) -> String {
if let i = fileName.lastIndex(of: ".") {
return String(fileName[i..<fileName.endIndex])
}
return ""
}
public static func getFileNameWithoutExtension(fileName: String) -> String {
if let i = fileName.lastIndex(of: ".") {
return String(fileName[fileName.startIndex..<i])
}
return fileName
}
public static func zipDirectory(zipPath: String, sourcePath: String) {
// todo
let pipe = Pipe()
let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/zip")
task.arguments = [zipPath, "-d", sourcePath]
task.standardOutput = pipe
let file = pipe.fileHandleForReading
do {
try task.run()
if let result = NSString(data: file.readDataToEndOfFile(), encoding: String.Encoding.utf8.rawValue) {
Log.info("unzip result: \(result as String)")
}
} catch {
Log.error(error.localizedDescription)
}
}
public static func unzipDirectory(zipPath: String, destinationPath: String) {
let pipe = Pipe()
let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/unzip")
task.arguments = [zipPath, "-d", destinationPath]
task.standardOutput = pipe
let file = pipe.fileHandleForReading
do {
try task.run()
if let result = NSString(data: file.readDataToEndOfFile(), encoding: String.Encoding.utf8.rawValue) {
Log.info("unzip result: \(result as String)")
}
} catch {
Log.error(error.localizedDescription)
}
}
}

View File

@ -0,0 +1,126 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public struct HtmlFormatter{
public static func format(src: String, indented: Bool = true) -> String{
let indexPairs = decompose(src: src)
return compose(src: src, pairs: indexPairs, indented: indented)
}
public static func decompose(src: String) -> Array<HtmlChunk>{
var pairs = Array<HtmlChunk>()
var quoted = false
var inTag = false
var start: Int = -1
var i = 0
for ch in src {
switch ch {
case "\"":
quoted = !quoted
i += 1
case "<":
if !quoted {
if start != -1 {
let code = src.substr(start+1, i).trim()
if !code.isEmpty {
let pair = HtmlChunk(type: .text, code: code)
pairs.append(pair)
}
}
inTag = true
start = i
}
i += 1
case ">":
if !quoted {
if !inTag {
print("tag end mismatch")
continue
}
var code = src.substr(start+1, i)
let selfClosing = code.hasSuffix("/")
if selfClosing{
code.removeLast()
}
var type = HtmlChunkType.startTag
if code.hasPrefix("/"){
type = .endTag
code.removeFirst()
}
if !code.isEmpty {
let pair = HtmlChunk(type: type, isSelfClosing: selfClosing, code: code)
pairs.append(pair)
}
inTag = false
start = i
}
i += 1
default:
i += 1
}
}
return pairs
}
public static func compose(src : String, pairs: Array<HtmlChunk>, indented: Bool = true) -> String{
var html = ""
var level = 0
var lastType = HtmlChunkType.text
for pair in pairs{
switch pair.type{
case .startTag:
html.append("\n")
if indented {
html.append(String(repeating: " ", count: level))
}
html.append("<")
html.append(pair.code)
if pair.isSelfClosing{
html.append("/")
}
html.append(">")
if !pair.isSelfClosing && !pair.code.hasPrefix("!"){
level += 1
}
case .endTag:
level -= 1
if lastType == .endTag {
html.append("\n")
if indented {
html.append(String(repeating: " ", count: level))
}
}
html.append("</")
html.append(pair.code)
html.append(">")
case .text:
html.append(pair.code)
}
lastType = pair.type
}
return html
}
public enum HtmlChunkType{
case startTag
case endTag
case text
}
public struct HtmlChunk {
var type : HtmlChunkType = .text
var isSelfClosing = false
var code = ""
}
}

View File

@ -0,0 +1,44 @@
//
// File.swift
//
//
// Created by Michael Rönnau on 28.03.21.
//
import Foundation
public struct Localizer{
public static var instance = Localizer()
public static func initialize(languages: Array<String>){
Log.info("initializing languages")
for lang in languages{
let path = Paths.baseDirectory.appendPath("Sources/SwiftyBandikaCL/" + lang + ".lproj")
if let bundle = Bundle(path: path){
instance.bundles[lang] = bundle
Log.info("found language bundle for '\(lang)'")
}
else{
Log.warn("language bundle not found at \(path)")
}
}
}
public var bundles = Dictionary<String, Bundle>()
public func localize(src: String) -> String{
NSLocalizedString(src, comment: "")
}
public func localize(src: String, language: String, def: String? = nil) -> String{
if let bundle = bundles[language]{
return bundle.localizedString(forKey: src, value: def, table: nil)
}
if let bundle = bundles["en"]{
return bundle.localizedString(forKey: src, value: def, table: nil)
}
return src
}
}

View File

@ -0,0 +1,43 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public protocol LogDelegate{
func updateLog()
}
public class Log{
public static var delegate : LogDelegate? = nil
public static var logFileUrl = Paths.logFile.toFileUrl()!
public static func info(_ string: String){
log(string,level: .info)
}
public static func warn(_ string: String){
log(string,level: .warn)
}
public static func error(_ string: String){
log(string,level: .error)
}
public static func error(error: Error){
log(error.localizedDescription,level: .error)
}
public static func log(_ string: String, level : LogLevel){
let msg = level.rawValue + Date().dateTimeString() + " " + string
Files.appendToFile(text: msg + "\n", url: logFileUrl)
delegate?.updateLog()
}
}

View File

@ -0,0 +1,15 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public enum LogLevel : String{
case info = "Info: "
case warn = "Warn: "
case error = "Error:"
}

View File

@ -0,0 +1,42 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
import Cocoa
extension NSImage {
public func resize(withSize targetSize: NSSize) -> NSImage? {
let frame = NSRect(x: 0, y: 0, width: targetSize.width, height: targetSize.height)
guard let representation = self.bestRepresentation(for: frame, context: nil, hints: nil) else {
return nil
}
let image = NSImage(size: targetSize, flipped: false, drawingHandler: { (_) -> Bool in
representation.draw(in: frame)
})
return image
}
public func resizeMaintainingAspectRatio(withSize targetSize: NSSize) -> NSImage? {
let newSize: NSSize
let widthRatio = targetSize.width / size.width
let heightRatio = targetSize.height / size.height
if widthRatio > heightRatio {
newSize = NSSize(width: floor(size.width * heightRatio),
height: floor(size.height * heightRatio))
} else {
newSize = NSSize(width: floor(size.width * widthRatio),
height: floor(size.height * widthRatio))
}
return resize(withSize: newSize)
}
}

View File

@ -0,0 +1,169 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
extension String {
public func localize() -> String {
Localizer.instance.localize(src: self)
}
public func localize(language: String, def: String? = nil) -> String {
Localizer.instance.localize(src: self, language: language, def: def)
}
public func toLocalizedHtml() -> String {
localize().toHtml()
}
public func toLocalizedHtml(language: String, def: String? = nil) -> String {
localize(language: language, def: def).toHtml()
}
public func trim() -> String {
trimmingCharacters(in: .whitespacesAndNewlines)
}
public func split(_ separator: Character) -> [String] {
self.split {
$0 == separator
}.map(String.init)
}
public func index(of string: String, from: Index) -> Index? {
range(of: string, options: [], range: from..<endIndex, locale: nil)?.lowerBound
}
public func makeRelativePath() -> String {
if hasPrefix("/") {
var s = self
s.removeFirst()
return s
}
return self
}
public func appendPath(_ path: String) -> String{
if path.isEmpty{
return self;
}
var newPath = self;
newPath.append("/")
newPath.append(path)
return newPath
}
public func lastPathComponent() -> String{
if var pos = lastIndex(of: "/") {
pos = index(after: pos)
return String(self[pos..<endIndex])
}
return self
}
public func toFileUrl() -> URL?{
URL(fileURLWithPath: self, isDirectory: false)
}
public func toDirectoryUrl() -> URL?{
URL(fileURLWithPath: self, isDirectory: true)
}
public func pathExtension() -> String {
if let idx = index(of: ".", from: startIndex) {
return String(self[index(after: idx)..<endIndex])
}
return self
}
public func pathWithoutExtension() -> String {
if let idx = index(of: ".", from: startIndex) {
return String(self[startIndex..<idx])
}
return self
}
public func unquote() -> String {
var scalars = unicodeScalars
if scalars.first == "\"" && scalars.last == "\"" && scalars.count >= 2 {
scalars.removeFirst()
scalars.removeLast()
return String(scalars)
}
return self
}
public func format(language: String, _ params: [String: String]?) -> String {
var s = ""
var p1: String.Index = startIndex
var p2: String.Index
while true {
if var varStart = index(of: "{{", from: p1) {
p2 = varStart
s.append(String(self[p1..<p2]))
varStart = self.index(varStart, offsetBy: 2)
if let varEnd = index(of: "}}", from: varStart) {
let key = String(self[varStart..<varEnd])
if key.contains("{{") {
p1 = p2
Log.error("parse error")
break
}
if key.hasPrefix("_") {
s.append(key.toLocalizedHtml(language: language))
} else if let value = params![key] {
s.append(value)
}
p1 = self.index(varEnd, offsetBy: 2)
} else {
p1 = p2
Log.error("parse error")
break
}
} else {
break
}
}
s.append(String(self[p1..<endIndex]))
return s
}
public func getKeyValueDict() -> [String: String] {
var attr = [String: String]()
var s = ""
var key = ""
var quoted = false
for ch in self {
switch ch {
case "\"":
if quoted {
if !key.isEmpty {
attr[key] = s
key = ""
s = ""
}
}
quoted = !quoted
case "=":
key = s
s = ""
case " ":
if quoted {
fallthrough
}
default:
s.append(ch)
}
}
return attr
}
}

View File

@ -0,0 +1,134 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
extension StringProtocol{
public func indexOf(_ input: String,
options: String.CompareOptions = .literal) -> String.Index? {
self.range(of: input, options: options)?.lowerBound
}
public func lastIndexOf(_ input: String) -> String.Index? {
indexOf(input, options: .backwards)
}
public func index(of string: String, from: Index) -> Index? {
range(of: string, options: [], range: from..<endIndex, locale: nil)?.lowerBound
}
public func charAt(_ i: Int) -> Character{
let idx = self.index(startIndex, offsetBy: i)
return self[idx]
}
public func substr(_ from: Int,_ to:Int) -> String{
if to < from{
return ""
}
let start = self.index(startIndex, offsetBy: from)
let end = self.index(startIndex, offsetBy: to)
return String(self[start..<end])
}
public func trim() -> String {
trimmingCharacters(in: .whitespacesAndNewlines)
}
public func toHtml() -> String {
var result = ""
for ch in self {
switch ch {
case "\"": result.append("&quot;")
case "'": result.append("&apos;")
case "&": result.append("&amp;")
case "<": result.append("&lt;")
case ">": result.append("&gt;")
default: result.append(ch)
}
}
return result
}
public func toHtmlMultiline() -> String {
self.toHtml().replacingOccurrences(of: "\n", with: "<br/>\n")
}
public func toUri() -> String {
var result = ""
var code = ""
for ch in self {
switch ch{
case "$" : code = "%24"
case "&" : code = "%26"
case ":" : code = "%3A"
case ";" : code = "%3B"
case "=" : code = "%3D"
case "?" : code = "%3F"
case "@" : code = "%40"
case " " : code = "%20"
case "\"" : code = "%5C"
case "<" : code = "%3C"
case ">" : code = "%3E"
case "#" : code = "%23"
case "%" : code = "%25"
case "~" : code = "%7E"
case "|" : code = "%7C"
case "^" : code = "%5E"
case "[" : code = "%5B"
case "]" : code = "%5D"
default: code = ""
}
if !code.isEmpty {
result.append(code)
}
else{
result.append(ch)
}
}
return result
}
public func toXml() -> String {
var result = ""
for ch in self {
switch ch {
case "\"": result.append("&quot;")
case "'": result.append("&apos;")
case "&": result.append("&amp;")
case "<": result.append("&lt;")
case ">": result.append("&gt;")
default: result.append(ch)
}
}
return result
}
public func toSafeWebName() -> String {
//todo complete this
let discardables = " [' \"><]+äöüÄÖÜß"
var result = ""
for ch in self {
var found = false
for dch in discardables{
if ch == dch{
found = true
break
}
}
if found{
continue
}
result.append(ch)
}
return result
}
}

View File

@ -0,0 +1,63 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class CkEditorController: Controller {
public static var instance = CkEditorController()
override public class var type: ControllerType {
get {
.ckeditor
}
}
override public func processRequest(method: String, id: Int?, request: Request) -> Response? {
switch method {
case "openLinkBrowser": return openLinkBrowser(id: id, request: request)
case "openImageBrowser": return openImageBrowser(id: id, request: request)
default:
return nil
}
}
public func openLinkBrowser(id: Int?, request: Request) -> Response {
if let data = request.getSessionContent(), id == data.id{
if !Right.hasUserEditRight(user: request.user, contentId: data.id) {
return Response(code: .forbidden)
}
return showBrowseLinks(request: request)
}
return Response(code: .badRequest)
}
public func openImageBrowser(id: Int?, request: Request) -> Response {
if let data = request.getSessionContent(), id == data.id {
if !Right.hasUserEditRight(user: request.user, contentId: data.id) {
return Response(code: .forbidden)
}
return showBrowseImages(request: request)
}
return Response(code: .badRequest)
}
public func showBrowseLinks(request: Request) -> Response{
request.setParam("type", "all")
request.addPageVar("callbackNum", String(request.getInt("CKEditorpublic funcNum")))
return ForwardResponse(page: "ckeditor/browseFiles.ajax", request: request)
}
public func showBrowseImages(request: Request) -> Response{
request.setParam("type", "image")
request.addPageVar("callbackNum", String(request.getInt("CKEditorpublic funcNum")))
return ForwardResponse(page: "ckeditor/browseFiles.ajax", request: request)
}
}

View File

@ -0,0 +1,74 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
extension Array where Element: ContentData{
public func toItemArray() -> Array<TypedContentItem>{
var array = Array<TypedContentItem>()
for data in self{
array.append(TypedContentItem(data: data))
}
return array
}
}
extension Array where Element: TypedContentItem{
public func toContentArray() -> Array<ContentData>{
var array = Array<ContentData>()
for item in self{
array.append(item.data)
}
return array
}
}
public class TypedContentItem: Codable{
private enum CodingKeys: CodingKey{
case type
case data
}
public var type : DataType
public var data : ContentData
init(data: ContentData){
type = data.type
self.data = data
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
type = try values.decode(DataType.self, forKey: .type)
switch type{
case .content:
data = try values.decode(ContentData.self, forKey: .data)
case .page:
data = try values.decode(PageData.self, forKey: .data)
case .fullpage:
data = try values.decode(FullPageData.self, forKey: .data)
case .templatepage:
data = try values.decode(TemplatePageData.self, forKey: .data)
default:
fatalError()
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type, forKey: .type)
try container.encode(data, forKey: .data)
}
}

View File

@ -0,0 +1,461 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class ContentContainer: DataContainer {
public static var instance = ContentContainer()
public static func initialize() {
Log.info("initializing content")
if !Files.fileExists(path: Paths.contentFile) {
if Files.copyFile(name: "content.json", fromDir: Paths.defaultContentDirectory, toDir: Paths.dataDirectory) {
Log.info("created default content")
} else {
Log.error("could not save default content")
}
}
if let str = Files.readTextFile(path: Paths.contentFile) {
Log.info("loading content")
if let container: ContentContainer = ContentContainer.fromJSON(encoded: str) {
Log.info("loaded content")
instance = container
Log.info("root data = \(instance.contentRoot)")
}
}
}
private enum ContentContainerCodingKeys: CodingKey {
case contentRoot
}
public var contentRoot: ContentData
private var contentDictionary = Dictionary<Int, ContentData>()
private var urlDictionary = Dictionary<String, ContentData>()
private var fileDictionary = Dictionary<Int, FileData>()
private let contentSemaphore = DispatchSemaphore(value: 1)
public required init() {
contentRoot = ContentData()
super.init()
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: ContentContainerCodingKeys.self)
contentRoot = try values.decode(TypedContentItem.self, forKey: .contentRoot).data
try super.init(from: decoder)
mapContent()
}
override public func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: ContentContainerCodingKeys.self)
try container.encode(TypedContentItem(data: contentRoot), forKey: .contentRoot)
}
private func lock() {
contentSemaphore.wait()
}
private func unlock() {
contentSemaphore.signal()
}
private func mapContent() {
var cDictionary = Dictionary<Int, ContentData>()
var uDictionary = Dictionary<String, ContentData>()
var fDictionary = Dictionary<Int, FileData>()
mapContent(data: contentRoot, contentDictionary: &cDictionary, urlDictionary: &uDictionary, fileDictionary: &fDictionary)
contentDictionary = cDictionary
urlDictionary = uDictionary
fileDictionary = fDictionary
Log.info("content mapped to ids and urls")
}
private func mapContent(data: ContentData, contentDictionary: inout Dictionary<Int, ContentData>, urlDictionary: inout Dictionary<String, ContentData>, fileDictionary: inout Dictionary<Int, FileData>) {
contentDictionary[data.id] = data
urlDictionary[data.getUrl()] = data
for child in data.children {
child.parentId = data.id
child.parentVersion = data.version
child.generatePath(parent: data)
mapContent(data: child, contentDictionary: &contentDictionary, urlDictionary: &urlDictionary, fileDictionary: &fileDictionary)
}
for file in data.files {
file.parentId = data.id
file.parentVersion = data.version
fileDictionary[file.id] = file
}
}
// content
public func getContent(id: Int) -> ContentData? {
contentDictionary[id]
}
public func getContent(id: Int, version: Int) -> ContentData? {
if let data = getContent(id: id) {
if data.version == version {
return data
}
}
return nil
}
public func getContent<T: ContentData>(id: Int, type: T.Type) -> T? {
if let data = getContent(id: id) as? T {
return data
}
return nil
}
public func getContent<T: ContentData>(id: Int, version: Int, type: T.Type) -> T? {
if let data: T = getContent(id: id, type: type) {
if data.version == version {
return data
}
}
return nil
}
public func getContent(url: String) -> ContentData? {
urlDictionary[url]
}
public func getContent<T: ContentData>(url: String, type: T.Type) -> T? {
if let data = getContent(url: url) as? T {
return data
}
return nil
}
public func getContents<T: ContentData>(type: T.Type) -> Array<T> {
contentDictionary.getTypedValues(type: type)
}
public func collectParentIds(contentId: Int) -> Array<Int> {
var ids = Array<Int>()
var content: ContentData? = getContent(id: contentId)
if content == nil {
return ids
}
content = getContent(id: content!.parentId)
while content != nil {
ids.append(content!.id)
content = getContent(id: content!.parentId)
}
return ids
}
// content changes
public func addContent(data: ContentData, userId: Int) -> Bool {
lock()
defer{
unlock()
}
if let parent = getContent(id: data.parentId, version: data.parentVersion) {
data.isNew = false
data.parentId = parent.id
data.parentVersion = parent.version
data.inheritRightsFromParent(parent: parent)
data.generatePath(parent: parent)
parent.children.append(data)
contentDictionary[data.id] = data
urlDictionary[data.getUrl()] = data
data.changerId = userId
data.changeDate = Application.instance.currentTime
setHasChanged()
return true
} else {
Log.warn("adding content - content not found: \(data.parentId)")
return false
}
}
public func updateContent(data: ContentData, userId: Int) -> Bool {
var success = false
lock()
defer{
unlock()
}
if let original = getContent(id: data.id, version: data.version, type: PageData.self) {
original.copyEditableAttributes(from: data)
original.copyPageAttributes(from: data)
original.increaseVersion()
original.changerId = userId
original.changeDate = Application.instance.currentTime
setHasChanged()
success = true
} else {
Log.warn("updating content - content not found: \(data.id)")
}
return success
}
public func publishContent(data: ContentData) -> Bool {
lock()
defer{
unlock()
}
if let contentData = data as? PageData {
contentData.publishDate = Application.instance.currentTime
setHasChanged()
}
return true
}
public func moveContent(data: ContentData, newParentId: Int, parentVersion: Int, userId: Int) -> Bool {
var success = false
lock()
defer{
unlock()
}
if let oldParent = getContent(id: data.parentId, version: data.parentVersion) {
if let newParent = getContent(id: newParentId, version: parentVersion) {
oldParent.children.remove(obj: data)
data.parentId = newParent.id
data.parentVersion = newParent.version
newParent.children.append(data)
data.inheritRightsFromParent(parent: newParent)
data.increaseVersion()
data.changerId = userId
data.changeDate = Application.instance.currentTime
setHasChanged()
success = true
}
} else {
Log.warn("moving content - content not found: \(data.parentId), \(newParentId)")
}
return success
}
public func updateChildRanking(data: ContentData, rankDictionary: Dictionary<Int, Int>, userId: Int) -> Bool {
var success = false
lock()
defer{
unlock()
}
for id in rankDictionary.keys {
for child in data.children {
if child.id == id {
if let ranking = rankDictionary[id] {
child.ranking = ranking
}
}
}
}
data.children.sort(by: { $0.ranking < $1.ranking })
setHasChanged()
success = true
return success
}
public func updateContentRights(data: ContentData, rightDictionary: Dictionary<Int, Right>, userId: Int) -> Bool {
var success = false
lock()
defer{
unlock()
}
if let original = getContent(id: data.id, version: data.version) {
original.groupRights.removeAll()
original.groupRights.addAll(from: rightDictionary)
original.changerId = userId
original.changeDate = Application.instance.currentTime
setHasChanged()
success = true
} else {
Log.warn("updating content rights - content not found: \(data.id)")
}
return success
}
public func removeContent(data: ContentData) -> Bool {
var success = true
lock()
defer{
unlock()
}
contentDictionary.remove(key: data.id)
for child in data.children {
success = success && removeContent(data: child)
}
for file in data.files {
fileDictionary.remove(key: file.id)
}
setHasChanged()
return success
}
// files
public func getFile(id: Int) -> FileData? {
fileDictionary[id]
}
public func getFile(id: Int, version: Int) -> FileData? {
if let data: FileData = getFile(id: id) {
if data.version == version {
return data
}
}
return nil
}
public func getFile<T: FileData>(id: Int, type: T.Type) -> T? {
fileDictionary.getTypedObject(key: id, type: type)
}
public func getFiles<T: FileData>(type: T.Type) -> Array<T> {
fileDictionary.getTypedValues(type: type)
}
// file changes
public func addFile(data: FileData, userId: Int) -> Bool {
var success = false
lock()
defer{
unlock()
}
if let parent = getContent(id: data.parentId, version: data.parentVersion) {
if data.file.exists() {
data.isNew = false
data.parentId = parent.id
data.parentVersion = parent.version
parent.files.append(data)
fileDictionary[data.id] = data
data.changerId = userId
data.changeDate = Application.instance.currentTime
setHasChanged()
success = true
} else {
Log.error("adding file - temp file not found: \(data.id)")
}
} else {
Log.error("adding file - content or file not found: \(data.parentId)")
}
return success
}
public func updateFile(data: FileData, userId: Int) -> Bool {
var success = false
lock()
defer{
unlock()
}
if let original = getFile(id: data.id, version: data.version) {
original.copyEditableAttributes(from: data)
if data.file.exists() {
original.file = data.file
original.fileType = FileType.fromContentType(contentType: original.contentType)
original.previewFile = data.previewFile
}
setHasChanged()
success = true
} else {
Log.warn("updating file - file not found: \(data.id)")
}
return success
}
public func moveFile(data: FileData, newParentId: Int, newParentVersion: Int, userId: Int) -> Bool {
var success = false
lock()
defer{
unlock()
}
if let oldParent = getContent(id: data.parentId, version: data.parentVersion), let newParent = getContent(id: newParentId, version: newParentVersion) {
oldParent.files.remove(obj: data)
newParent.files.append(data)
data.changerId = userId
data.changeDate = Application.instance.currentTime
data.increaseVersion()
setHasChanged()
success = true
} else {
Log.warn("moving file - content not found: \(data.parentId), \(newParentId)")
}
return success
}
public func removeFile(data: FileData) -> Bool {
var success = false
lock()
defer{
unlock()
}
if let parent = getContent(id: data.parentId, version: data.parentVersion) {
parent.files.remove(obj: data)
fileDictionary.remove(key: data.id)
setHasChanged()
success = true
} else {
Log.warn("removing file from content - content not found: \(data.parentId)")
}
return success
}
// binary files
public func moveTempFiles() -> Bool {
var success = true
for file: FileData in fileDictionary.values {
success = success && file.moveTempFiles()
}
return success
}
public func cleanupFiles() {
var fileNames = Set<String>()
fileNames.insert("tmp")
for file in fileDictionary.values {
fileNames.insert(file.idFileName)
fileNames.insert(file.previewFileName)
}
if !Files.deleteAllFiles(dir: Paths.fileDirectory, except: fileNames) {
Log.warn("not all files could be deleted")
}
}
override public func checkChanged() {
if (changed) {
if save() {
Log.info("contents saved")
changed = false
}
}
}
override public func save() -> Bool {
if !moveTempFiles() {
Log.warn("not all files saved")
return false
}
Log.info("saving content data")
lock()
defer{
unlock()
}
let json = toJSON()
if !Files.saveFile(text: json, path: Paths.contentFile) {
Log.warn("content file could not be saved")
return false
}
return true
}
}

View File

@ -0,0 +1,473 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class ContentController: Controller {
override public func processRequest(method: String, id: Int?, request: Request) -> Response? {
switch method {
case "show": return show(id: id, request: request)
case "openCreateContentData": return openCreateContentData(request: request)
case "openEditContentData": return openEditContentData(id: id, request: request)
case "saveContentData": return saveContentData(id: id, request: request)
case "openEditRights": return openEditRights(id: id, request: request)
case "saveRights": return saveRights(id: id, request: request)
case "cutContent": return cutContent(id: id, request: request)
case "copyContent": return copyContent(id: id, request: request)
case "pasteContent": return pasteContent(request: request)
case "deleteContent": return deleteContent(id: id, request: request)
case "openSortChildPages": return openSortChildPages(id: id, request: request)
case "saveChildPageRanking": return saveChildPageRanking(id: id, request: request)
case "showDraft": return showDraft(id: id, request: request)
case "showPublished": return showPublished(id: id, request: request)
case "publishPage": return publishPage(id: id, request: request)
default:
return nil
}
}
public func show(id: Int?, request: Request) -> Response? {
if let id = id, let content = ContentContainer.instance.getContent(id: id) {
request.setContent(content)
request.addPageVar("language", Statics.instance.defaultLocale.languageCode ?? "en")
request.addPageVar("title", Statics.title.toHtml())
request.addPageVar("keywords", content.keywords.toHtml())
request.addPageVar("description", content.description.trim().toHtml())
let master = TemplateCache.getTemplate(type: TemplateType.master, name: content.master)
if let html = master?.getHtml(request: request) {
return Response(html: HtmlFormatter.format(src: html, indented: true))
}
} else {
Log.warn("content id not found : \(id ?? 0)")
}
return Response(code: .notFound)
}
public func openCreateContentData(request: Request) -> Response {
let parentId = request.getInt("parentId")
if let parent = ContentContainer.instance.getContent(id: parentId) {
if !Right.hasUserEditRight(user: request.user, content: parent) {
return Response(code: .forbidden)
}
let type = request.getString("type")
if let type = DataType(rawValue: type), let data = DataFactory.create(type: type) as? ContentData{
data.setCreateValues(parent: parent, request: request)
data.parentId = parent.id
data.parentVersion = parent.version
request.setSessionContent(data)
if let controller = ControllerFactory.getDataController(type: data.type) as? ContentController{
return controller.showEditContent(contentData: data, request: request)
}
else{
Log.error("controller type not found: \(type)")
}
}
else{
Log.error("data type not found: \(type)")
}
}
return Response(code: .badRequest)
}
public func openEditContentData(id: Int?, request: Request) -> Response {
if let id = id {
if !Right.hasUserEditRight(user: request.user, contentId: id) {
return Response(code: .forbidden)
}
if let original = ContentContainer.instance.getContent(id: id) {
if let data = DataFactory.create(type: original.type) as? PageData {
data.copyFixedAttributes(from: original)
data.copyEditableAttributes(from: original)
request.setSessionContent(data)
return showEditContent(contentData: data, request: request)
}
}
}
return Response(code: .badRequest)
}
public func saveContentData(id: Int?, request: Request) -> Response {
if let id = id, let data = request.getSessionContent(type: PageData.self), data.id == id {
if (request.hasFormError) {
return showEditContent(contentData: data, request: request)
}
if data.isNew {
if !Right.hasUserEditRight(user: request.user, contentId: data.parentId) {
return Response(code: .forbidden)
}
} else {
if !Right.hasUserEditRight(user: request.user, content: data) {
return Response(code: .forbidden)
}
}
data.readRequest(request)
if request.hasFormError {
return showEditContent(contentData: data, request: request)
}
if data.isNew {
if !ContentContainer.instance.addContent(data: data, userId: request.userId) {
Log.warn("data could not be added");
request.setMessage("_versionError", type: .danger)
return showEditContent(contentData: data, request: request)
}
data.isNew = false
} else {
if !ContentContainer.instance.updateContent(data: data, userId: request.userId) {
Log.warn("original data not found for update")
request.setMessage("_versionError", type: .danger)
return showEditContent(contentData: data, request: request)
}
}
request.removeSessionContent()
request.setMessage("_contentSaved", type: .success)
return CloseDialogResponse(url: "/ctrl/admin/openContentAdministration?contentId=\(id)", request: request)
}
return Response(code: .badRequest)
}
public func openEditRights(id: Int?, request: Request) -> Response {
if let id = id, let data = ContentContainer.instance.getContent(id: id) {
if !Right.hasUserEditRight(user: request.user, contentId: data.id) {
return Response(code: .forbidden)
}
request.setSessionContent(data)
request.setContent(data)
return showEditRights(contentData: data, request: request)
}
return Response(code: .badRequest)
}
public func saveRights(id: Int?, request: Request) -> Response {
if let id = id, let data = ContentContainer.instance.getContent(id: id) {
let version = request.getInt("version")
if data.version != version {
Log.warn("original data not found for update.")
request.setMessage("_saveError", type: .danger)
return showEditRights(contentData: data, request: request)
}
if !Right.hasUserEditRight(user: request.user, content: data) {
return Response(code: .forbidden)
}
var rights = Dictionary<Int, Right>()
let groups = UserContainer.instance.groups
for group in groups {
if group.id <= GroupData.ID_MAX_FINAL {
continue
}
let value = request.getInt("groupright_\(group.id)")
if let right = Right(rawValue: value) {
rights[group.id] = right
}
}
if !ContentContainer.instance.updateContentRights(data: data, rightDictionary: rights, userId: request.userId) {
Log.warn("content rights could not be updated")
request.setMessage("_saveError", type: .danger)
return showEditRights(contentData: data, request: request)
}
request.removeSessionContent()
request.setMessage("_rightsSaved", type: .success);
return CloseDialogResponse(url: "/ctrl/admin/openContentAdministration?contentId=\(id)", request: request)
}
return Response(code: .badRequest)
}
public func cutContent(id: Int?, request: Request) -> Response {
if let id = id {
if id == ContentData.ID_ROOT {
return showContentAdministration(contentId: ContentData.ID_ROOT, request: request)
}
if let data = ContentContainer.instance.getContent(id: id) {
if !Right.hasUserEditRight(user: request.user, content: data) {
return Response(code: .forbidden)
}
Clipboard.instance.setData(type: .content, data: data)
return showContentAdministration(contentId: data.id, request: request)
}
}
return Response(code: .badRequest)
}
public func copyContent(id: Int?, request: Request) -> Response {
if let id = id {
if (id == ContentData.ID_ROOT) {
return showContentAdministration(contentId: ContentData.ID_ROOT, request: request)
}
if let srcData = ContentContainer.instance.getContent(id: id) {
if !Right.hasUserEditRight(user: request.user, content: srcData) {
return Response(code: .forbidden)
}
if let data = DataFactory.create(type: srcData.type) as? ContentData{
if ContentContainer.instance.getContent(id: srcData.parentId) != nil {
data.copyEditableAttributes(from: srcData)
data.setCreateValues(request: request)
//marking as copy
data.parentId = 0
data.parentVersion = 0
Clipboard.instance.setData(type: .content, data: data)
return showContentAdministration(contentId: data.id, request: request)
}
}
}
}
return Response(code: .badRequest)
}
public func pasteContent(request: Request) -> Response {
let parentId = request.getInt("parentId")
let parentVersion = request.getInt("parentVersion")
if let data = Clipboard.instance.getData(type: .content) as? ContentData {
if !Right.hasUserEditRight(user: request.user, contentId: parentId) {
return Response(code: .forbidden)
}
let parentIds = ContentContainer.instance.collectParentIds(contentId: data.id)
if parentIds.contains(data.id) {
request.setMessage("_actionNotExcecuted", type: .danger)
return showContentAdministration(request: request)
}
if data.parentId != 0 {
//has been cut
if !ContentContainer.instance.moveContent(data: data, newParentId: parentId, parentVersion: parentVersion, userId: request.userId) {
request.setMessage("_actionNotExcecuted", type: .danger)
return showContentAdministration(request: request)
}
} else {
// has been copied
data.parentId = parentId
data.parentVersion = parentVersion
if !ContentContainer.instance.addContent(data: data, userId: request.userId) {
request.setMessage("_actionNotExcecuted", type: .danger)
return showContentAdministration(request: request);
}
}
Clipboard.instance.removeData(type: .content)
request.setMessage("_contentPasted", type: .success)
return showContentAdministration(contentId: data.id, request: request);
}
return Response(code: .badRequest)
}
public func deleteContent(id: Int?, request: Request) -> Response {
if let id = id {
if id == ContentData.ID_ROOT {
request.setMessage("_notDeletable", type: .danger)
return showContentAdministration(request: request)
}
if let data = ContentContainer.instance.getContent(id: id) {
if !Right.hasUserEditRight(user: request.user, content: data) {
return Response(code: .forbidden)
}
if let parent = ContentContainer.instance.getContent(id: data.parentId) {
parent.children.remove(obj: data)
_ = ContentContainer.instance.removeContent(data: data)
request.setParam("contentId", String(parent.id))
request.setMessage("_contentDeleted", type: .success)
return showContentAdministration(contentId: parent.id, request: request)
}
}
}
return Response(code: .badRequest)
}
public func openSortChildPages(id: Int?, request: Request) -> Response {
if let id = id {
if let data = ContentContainer.instance.getContent(id: id) {
if !Right.hasUserEditRight(user: request.user, content: data) {
return Response(code: .forbidden)
}
request.setSessionContent(data)
return showSortChildContents(contentData: data, request: request)
}
}
return Response(code: .badRequest)
}
public func saveChildPageRanking(id: Int?, request: Request) -> Response {
if let id = id, let data = request.getSessionContent(), id == data.id {
if !Right.hasUserEditRight(user: request.user, content: data) {
return Response(code: .forbidden)
}
var rankMap = Dictionary<Int, Int>()
for child in data.children {
let ranking = request.getInt("select\(child.id)", def: -1)
rankMap[child.id] = ranking
}
if !ContentContainer.instance.updateChildRanking(data: data, rankDictionary: rankMap, userId: request.userId) {
Log.warn("sorting did not succeed");
return showSortChildContents(contentData: data, request: request)
}
request.removeSessionContent()
request.setMessage("_newRankingSaved", type: .success);
return CloseDialogResponse(url: "/ctrl/admin/openContentAdministration?contentId=\(id)", request: request)
}
return Response(code: .badRequest)
}
public func showEditPage(id: Int?, request: Request) -> Response {
if let id = id {
if !Right.hasUserEditRight(user: request.user, contentId: id) {
return Response(code: .forbidden)
}
request.viewType = .edit
return show(id: id, request: request) ?? Response(code: .internalServerError)
}
return Response(code: .badRequest)
}
public func showDraft(id: Int?, request: Request) -> Response {
if let id = id, let data = ContentContainer.instance.getContent(id: id) {
if !Right.hasUserReadRight(user: request.user, contentId: data.id) {
return Response(code: .forbidden)
}
request.viewType = .showDraft
return show(id: data.id, request: request) ?? Response(code: .internalServerError)
}
return Response(code: .badRequest)
}
public func showPublished(id: Int?, request: Request) -> Response {
if let id = id, let data = ContentContainer.instance.getContent(id: id) {
if !Right.hasUserReadRight(user: request.user, contentId: data.id) {
return Response(code: .forbidden)
}
request.viewType = .showPublished
return show(id: data.id, request: request) ?? Response(code: .internalServerError)
}
return Response(code: .badRequest)
}
public func publishPage(id: Int?, request: Request) -> Response {
if let id = id, let data = ContentContainer.instance.getContent(id: id) as? PageData {
if !Right.hasUserApproveRight(user: request.user, contentId: data.id) {
return Response(code: .forbidden)
}
data.createPublishedContent(request: request)
data.changerId = request.userId
data.changeDate = Application.instance.currentTime
_ = ContentContainer.instance.publishContent(data: data)
return show(id: data.id, request: request) ?? Response(code: .internalServerError)
}
return Response(code: .badRequest)
}
public func showEditContent(contentData: ContentData, request: Request) -> Response {
if let cnt = request.getSessionContent() {
request.setContent(cnt)
}
request.addPageVar("url", "/ctrl/content/saveContentData/\(contentData.id)")
setEditPageVars(contentData: contentData, request: request)
return ForwardResponse(page: "content/editContentData.ajax", request: request)
}
public func setEditPageVars(contentData: ContentData, request: Request) {
request.addPageVar("id", String(contentData.id))
request.addPageVar("creationDate", contentData.creationDate.dateTimeString())
if let user = UserContainer.instance.getUser(id: contentData.creatorId) {
request.addPageVar("creatorName", user.name.toHtml())
}
request.addPageVar("changeDate", String(contentData.changeDate.dateTimeString()))
if let user = UserContainer.instance.getUser(id: contentData.changerId) {
request.addPageVar("changerName", user.name.toHtml())
}
request.addPageVar("displayName", contentData.displayName.toHtml())
request.addPageVar("description", contentData.description.trim().toHtml())
request.addConditionalPageVar("isOpenSelected", "selected", if: contentData.accessType == ContentData.ACCESS_TYPE_OPEN)
request.addConditionalPageVar("isInheritsSelected", "selected", if: contentData.accessType == ContentData.ACCESS_TYPE_INHERITS)
request.addConditionalPageVar("isIndividualSelected", "selected", if: contentData.accessType == ContentData.ACCESS_TYPE_INDIVIDUAL)
request.addConditionalPageVar("isNoneNavSelected", "selected", if: contentData.navType == ContentData.NAV_TYPE_NONE)
request.addConditionalPageVar("isHeaderNavSelected", "selected", if: contentData.navType == ContentData.NAV_TYPE_HEADER)
request.addConditionalPageVar("isFooterNavSelected", "selected", if: contentData.navType == ContentData.NAV_TYPE_FOOTER)
request.addPageVar("active", contentData.active ? "true" : "false")
var str = FormSelectTag.getOptionHtml(request: request, value: "", isSelected: contentData.master.isEmpty, text: "_pleaseSelect".toLocalizedHtml(language: request.language))
if let masters = TemplateCache.getTemplates(type: .master) {
for masterName in masters.keys {
str.append(FormSelectTag.getOptionHtml(request: request, value: masterName.toHtml(), isSelected: contentData.master == masterName, text: masterName.toHtml()))
}
}
request.addPageVar("masterOptions", str)
}
public func showEditRights(contentData: ContentData, request: Request) -> Response {
request.addPageVar("url", "/ctrl/\(contentData.type.rawValue)/saveRights/\(contentData.id)?version=\(contentData.version)")
var html = ""
for group in UserContainer.instance.groups {
if group.id <= GroupData.ID_MAX_FINAL {
continue
}
let name = "groupright_\(group.id)"
let lineTag = FormLineTag()
let rights = contentData.groupRights[group.id]
lineTag.label = group.name.toHtml()
lineTag.padded = true
html.append(lineTag.getStartHtml(request: request))
let radioTag = FormRadioTag()
radioTag.name = name
radioTag.checked = rights != nil
radioTag.value = ""
html.append(radioTag.getStartHtml(request: request))
html.append("_rightnone".toLocalizedHtml(language: request.language))
html.append(radioTag.getEndHtml(request: request))
html.append("<br/>")
radioTag.checked = rights?.includesRight(right: .READ) ?? false
radioTag.value = String(Right.READ.rawValue)
html.append(radioTag.getStartHtml(request: request))
html.append("_rightread".toLocalizedHtml(language: request.language))
html.append(radioTag.getEndHtml(request: request))
html.append("<br/>")
radioTag.checked = rights?.includesRight(right: .EDIT) ?? false
radioTag.value = String(Right.EDIT.rawValue)
html.append(radioTag.getStartHtml(request: request))
html.append("_rightedit".toLocalizedHtml(language: request.language))
html.append(radioTag.getEndHtml(request: request))
html.append("<br/>")
radioTag.checked = rights?.includesRight(right: .APPROVE) ?? false
radioTag.value = String(Right.APPROVE.rawValue)
html.append(radioTag.getStartHtml(request: request))
html.append("_rightapprove".toLocalizedHtml(language: request.language))
html.append(radioTag.getEndHtml(request: request))
html.append(lineTag.getEndHtml(request: request))
}
request.addPageVar("groupRights", html)
return ForwardResponse(page: "content/editGroupRights.ajax", request: request);
}
public func showSortChildContents(contentData: ContentData, request: Request) -> Response {
request.addPageVar("url", "/ctrl/\(contentData.type.rawValue)/saveChildPageRanking/\(contentData.id)?version=\(contentData.version)")
var childSortList = Array<(Int,String)>()
for subpage in contentData.children {
childSortList.append((subpage.id, subpage.name))
}
var idx = 0
var html = ""
for pair in childSortList {
let name = "select" + String(pair.0)
let onchange = "setRanking(" + pair.1 + ");"
let lineTag = FormLineTag()
lineTag.label = pair.1.toHtml()
lineTag.padded = true
html.append(lineTag.getStartHtml(request: request))
let select = FormSelectTag()
select.name = name
select.label = String(idx)
select.onChange = onchange
html.append(FormSelectTag.preControlHtml.format(language: request.language, [
"name": name,
"onchange": onchange]))
for i in 0..<childSortList.count {
html.append(FormSelectTag.getOptionHtml(request: request, value: String(i), isSelected: i == idx, text: String(i + 1)))
}
html.append(FormSelectTag.postControlHtml)
html.append(lineTag.getEndHtml(request: request))
idx += 1
}
request.addPageVar("sortContents", html)
return ForwardResponse(page: "content/sortChildContents.ajax", request: request);
}
}

View File

@ -0,0 +1,202 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class ContentData : BaseData{
public static var DEFAULT_MASTER = "defaultMaster"
public static var ACCESS_TYPE_OPEN = "OPEN";
public static var ACCESS_TYPE_INHERITS = "INHERIT";
public static var ACCESS_TYPE_INDIVIDUAL = "INDIVIDUAL";
public static var NAV_TYPE_NONE = "NONE";
public static var NAV_TYPE_HEADER = "HEADER";
public static var NAV_TYPE_FOOTER = "FOOTER";
public static var ID_ROOT : Int = 1
private enum ContentDataCodingKeys: CodingKey{
case name
case displayName
case description
case keywords
case master
case accessType
case navType
case active
case groupRights
case children
case files
}
// base data
public var name : String
public var displayName : String
public var description : String
public var keywords : String
public var master : String
public var accessType : String
public var navType : String
public var active : Bool
public var groupRights : Dictionary<Int, Right>
public var children : Array<ContentData>
public var files : Array<FileData>
public var parentId = 0
public var parentVersion = 0
public var ranking = 0
public var path = ""
public var childTypes : [DataType]{
get{
return [.fullpage, .templatepage]
}
}
override public var type : DataType{
get {
.content
}
}
public var openAccess : Bool{
get{
accessType == ContentData.ACCESS_TYPE_OPEN
}
}
public var isRoot : Bool{
get{
id == ContentData.ID_ROOT
}
}
override init(){
name = ""
displayName = ""
description = ""
keywords = ""
master = ContentData.DEFAULT_MASTER
accessType = ContentData.ACCESS_TYPE_OPEN
navType = ContentData.NAV_TYPE_NONE
active = true
groupRights = Dictionary<Int, Right>()
children = Array<ContentData>()
files = Array<FileData>()
super.init()
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: ContentDataCodingKeys.self)
name = try values.decodeIfPresent(String.self, forKey: .name) ?? ""
displayName = try values.decodeIfPresent(String.self, forKey: .displayName) ?? ""
description = try values.decodeIfPresent(String.self, forKey: .description) ?? ""
keywords = try values.decodeIfPresent(String.self, forKey: .keywords) ?? ""
master = try values.decodeIfPresent(String.self, forKey: .master) ?? "defaultMaster"
accessType = try values.decodeIfPresent(String.self, forKey: .accessType) ?? "OPEN"
navType = try values.decodeIfPresent(String.self, forKey: .navType) ?? "NONE"
active = try values.decodeIfPresent(Bool.self, forKey: .active) ?? false
groupRights = try values.decodeIfPresent(Dictionary<Int, Right>.self, forKey: .groupRights) ?? Dictionary<Int, Right>()
let items = try values.decodeIfPresent(Array<TypedContentItem>.self, forKey: .children) ?? Array<TypedContentItem>()
children = items.toContentArray()
files = try values.decodeIfPresent(Array<FileData>.self, forKey: .files) ?? Array<FileData>()
try super.init(from: decoder)
}
override public func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: ContentDataCodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(displayName, forKey: .displayName)
try container.encode(description, forKey: .description)
try container.encode(keywords, forKey: .keywords)
try container.encode(master, forKey: .master)
try container.encode(accessType, forKey: .accessType)
try container.encode(navType, forKey: .navType)
try container.encode(active, forKey: .active)
try container.encode(groupRights, forKey: .groupRights)
let contentItems = children.toItemArray()
try container.encode(contentItems, forKey: .children)
try container.encode(files, forKey: .files)
}
override public func copyEditableAttributes(from data: TypedData) {
super.copyEditableAttributes(from: data)
if let contentData = data as? ContentData{
name = contentData.name
displayName = contentData.displayName
description = contentData.description
keywords = contentData.keywords
master = contentData.master
accessType = contentData.accessType
navType = contentData.navType
active = contentData.active
groupRights.removeAll()
groupRights.addAll(from: contentData.groupRights)
}
}
public func setCreateValues(parent: ContentData, request: Request) {
super.setCreateValues(request: request)
parentId = parent.id
parentVersion = parent.version
inheritRightsFromParent(parent: parent)
}
public func generatePath(parent: ContentData?) {
if let parent = parent{
path = parent.path + "/" + name
}
}
public func getUrl() -> String{
if path.isEmpty{
return "/home.html"
}
return path + ".html";
}
public func inheritRightsFromParent(parent: ContentData?){
groupRights.removeAll()
if let parent = parent {
groupRights.addAll(from: parent.groupRights)
}
}
override public func readRequest(_ request: Request) {
displayName = request.getString("displayName").trim()
name = displayName.toSafeWebName()
print(name)
description = request.getString("description")
keywords = request.getString("keywords")
master = request.getString("master")
if name.isEmpty{
request.addIncompleteField("name")
}
accessType = request.getString("accessType")
navType = request.getString("navType")
active = request.getBool("active")
}
public func getChildren<T: ContentData>(cls: T.Type) -> Array<T>{
children.getTypedArray(type: cls)
}
public func getFiles<T: FileData>(cls: T.Type) -> Array<T>{
files.getTypedArray(type: cls)
}
public func displayContent(request: Request) -> String{
""
}
}

View File

@ -0,0 +1,253 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FileController: Controller {
public static var instance = FileController()
override public class var type: ControllerType {
get {
.file
}
}
override public func processRequest(method: String, id: Int?, request: Request) -> Response? {
switch method {
case "show": return show(id: id, request: request)
case "showPreview": return showPreview(id: id, request: request)
case "openCreateFile": return openCreateFile(request: request)
case "openEditFile": return openEditFile(id: id, request: request)
case "saveFile": return saveFile(request: request)
case "cutFile": return cutFile(id: id, request: request)
case "copyFile": return copyFile(id: id, request: request)
case "pasteFile": return pasteFile(request: request)
case "deleteFile": return deleteFile(id: id, request: request)
default:
return nil
}
}
public func show(request: Request) -> Response {
var path = request.path
path.removeFirst(Router.filesPrefix.count)
path = path.pathWithoutExtension()
var isPreview = false
if path.hasPrefix("preview"){
isPreview = true
path.removeFirst("preview".count)
}
if let id = Int(path) {
if isPreview{
return showPreview(id: id, request: request)
}
else {
return show(id: id, request: request)
}
}
else{
Log.error("id in path not found: \(path)")
}
return Response(code: .notFound)
}
public func show(id: Int?, request: Request) -> Response {
if let id = id, let file = ContentContainer.instance.getFile(id: id) {
if !Right.hasUserReadRight(user: request.user, contentId: file.parentId) {
return Response(code: .forbidden)
}
Log.info("loading file \(id)")
if let data: Data = Files.readFile(path: file.file.path) {
let download = request.getBool("download")
let contentType = MimeType.from(file.file.path)
return Response(data: data, fileName: file.fileName, contentType: contentType, download: download)
}
Log.info("reading file for id \(id) failed")
}
return Response(code: .notFound)
}
public func showPreview(id: Int?, request: Request) -> Response {
if let id = id, let file = ContentContainer.instance.getFile(id: id) {
if !Right.hasUserReadRight(user: request.user, contentId: file.parentId) {
return Response(code: .forbidden)
}
Log.info("loading preview file \(id)")
if let pvf = file.previewFile, let data: Data = Files.readFile(path: pvf.path) {
let contentType = MimeType.from(file.file.path)
return Response(data: data, fileName: file.fileName, contentType: contentType)
}
Log.info("reading preview file for id \(id) failed")
}
return Response(code: .notFound)
}
public func openCreateFile(request: Request) -> Response {
let parentId = request.getInt("parentId")
if let parent = ContentContainer.instance.getContent(id: parentId) {
if !Right.hasUserEditRight(user: request.user, content: parent) {
return Response(code: .forbidden)
}
let data = FileData()
data.setCreateValues(request: request)
data.parentId = parent.id
data.parentVersion = parent.version
request.setSessionFile(data)
return showEditFile(file: data, request: request)
}
return Response(code: .badRequest)
}
public func openEditFile(id: Int?, request: Request) -> Response {
if let id = id, let original = ContentContainer.instance.getFile(id: id) {
let data = FileData()
data.copyFixedAttributes(from: original)
data.copyEditableAttributes(from: original)
if !Right.hasUserEditRight(user: request.user, contentId: original.parentId) {
return Response(code: .forbidden)
}
request.setSessionFile(data)
return showEditFile(file: data, request: request)
}
return Response(code: .badRequest)
}
public func saveFile(request: Request) -> Response {
if let data = request.getSessionFile() {
if !Right.hasUserEditRight(user: request.user, contentId: data.parentId) {
return Response(code: .forbidden)
}
data.readRequest(request)
if (request.hasFormError) {
return showEditFile(file: data,request: request)
}
if data.isNew {
if !ContentContainer.instance.addFile(data:data, userId: request.userId) {
Log.warn("data could not be added")
request.setMessage("_versionError", type: .danger)
return showEditFile(file: data,request: request)
}
} else {
if !ContentContainer.instance.updateFile(data: data, userId: request.userId) {
Log.warn("data could not be updated");
request.setMessage("_versionError", type: .danger)
return showEditFile(file: data,request: request);
}
}
request.removeSessionFile()
request.setMessage("_fileSaved", type: .success);
request.setParam("fileId", String(data.id))
return CloseDialogResponse(url: "/ctrl/admin/openContentAdministration", request: request)
}
return Response(code: .badRequest)
}
public func cutFile(id: Int?, request: Request) -> Response {
if let id = id, let data = ContentContainer.instance.getFile(id: id) {
if !Right.hasUserEditRight(user: request.user, contentId: data.parentId) {
return Response(code: .forbidden)
}
Clipboard.instance.setData(type: .file, data: data)
return showContentAdministration(contentId: data.parentId, request: request)
}
return Response(code: .badRequest)
}
public func copyFile(id: Int?, request: Request) -> Response {
if let id = id, let original = ContentContainer.instance.getFile(id: id, type: FileData.self) {
if !Right.hasUserEditRight(user: request.user, contentId: original.parentId) {
return Response(code: .forbidden)
}
let data = FileData()
data.copyFixedAttributes(from: original)
data.copyEditableAttributes(from: original)
data.setCreateValues(request: request)
// marking as copy
data.parentId = 0
data.parentVersion = 0
var success = Files.copyFile(from: original.file.path, to: data.file.path)
if original.isImage, let opf = original.previewFile, let npf = data.previewFile {
success = success && Files.copyFile(from: opf.path, to: npf.path)
}
if !success {
return Response(code: .internalServerError)
}
Clipboard.instance.setData(type: DataType.file, data: data)
return showContentAdministration(contentId: data.id, request: request)
}
return Response(code: .badRequest)
}
public func pasteFile(request: Request) -> Response {
let parentId = request.getInt("parentId")
let parentVersion = request.getInt("parentVersion")
if !Right.hasUserEditRight(user: request.user, contentId: parentId) {
return Response(code: .forbidden)
}
if ContentContainer.instance.getContent(id: parentId) != nil {
if let data = Clipboard.instance.getData(type: .file) as? FileData {
if data.parentId != 0 {
//has been cut
if !ContentContainer.instance.moveFile(data: data, newParentId: parentId, newParentVersion: parentVersion, userId: request.userId) {
request.setMessage("_actionNotExcecuted", type: .danger)
return showContentAdministration(contentId: data.id,request: request)
}
} else {
//has been copied
data.parentId = parentId
data.parentVersion = parentVersion
if !ContentContainer.instance.addFile(data: data, userId: request.userId) {
request.setMessage("_actionNotExcecuted", type: .danger);
return showContentAdministration(contentId: data.id,request: request)
}
}
Clipboard.instance.removeData(type: .file)
request.setMessage("_filePasted", type: .success)
return showContentAdministration(contentId: data.id, request: request)
}
}
return Response(code: .badRequest)
}
public func deleteFile(id: Int?, request: Request) -> Response {
if let id = id, let file = ContentContainer.instance.getFile(id: id) {
if !Right.hasUserReadRight(user: request.user, contentId: file.parentId) {
return Response(code: .forbidden)
}
_ = ContentContainer.instance.removeFile(data: file)
request.setParam("contentId", String(file.parentId))
request.setMessage("_fileDeleted", type: .success)
return showContentAdministration(contentId: file.parentId, request: request)
}
return Response(code: .badRequest)
}
public func showEditFile(file: FileData, request: Request) -> Response {
request.addPageVar("url", "/ctrl/file/saveFile/\(file.id)")
setPageVars(file: file, request: request)
return ForwardResponse(page: "file/editFile.ajax", request: request)
}
public func setPageVars(file: FileData, request: Request) {
request.addPageVar("id", String(file.id))
request.addPageVar("creationDate", file.creationDate.dateTimeString())
if let user = UserContainer.instance.getUser(id: file.creatorId) {
request.addPageVar("creatorName", user.name.toHtml())
}
request.addPageVar("changeDate", String(file.changeDate.dateTimeString()))
if let user = UserContainer.instance.getUser(id: file.changerId) {
request.addPageVar("changerName", user.name.toHtml())
}
request.addPageVar("fileName", file.fileName.toHtml())
request.addPageVar("fileRequired", String(file.isNew))
request.addPageVar("displayName", file.displayName.toHtml())
request.addPageVar("description", file.description.trim().toHtml())
}
}

View File

@ -0,0 +1,203 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FileData: BaseData {
public static var MAX_PREVIEW_SIDE: Int = 200
private enum ContentDataCodingKeys: CodingKey {
case fileName
case displayName
case description
case contentType
case fileType
}
public var fileName : String
public var displayName : String
public var description : String
public var contentType: String
public var fileType: FileType
public var parentId : Int
public var parentVersion : Int
public var maxWidth : Int
public var maxHeight : Int
public var maxPreviewSide : Int
public var live = false
public var file : DiskFile!
public var previewFile: DiskFile? = nil
public var idFileName: String {
get {
String(id) + Files.getExtension(fileName: fileName)
}
}
public var previewFileName: String {
get {
"preview" + String(id) + ".jpg"
}
}
public var url: String {
get {
"/files/" + idFileName
}
}
public var previewUrl: String {
get {
"/files/" + previewFileName
}
}
public var isImage: Bool {
get {
fileType == .image
}
}
override public var type: DataType {
get {
.file
}
}
override init() {
fileName = ""
displayName = ""
description = ""
contentType = ""
fileType = FileType.unknown
parentId = 0
parentVersion = 0
maxWidth = 0
maxHeight = 0
maxPreviewSide = FileData.MAX_PREVIEW_SIDE
super.init()
file = DiskFile(name: idFileName, live: false)
if isImage{
previewFile = DiskFile(name: previewFileName, live: false)
}
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: ContentDataCodingKeys.self)
fileName = try values.decodeIfPresent(String.self, forKey: .fileName) ?? ""
displayName = try values.decodeIfPresent(String.self, forKey: .displayName) ?? ""
description = try values.decodeIfPresent(String.self, forKey: .description) ?? ""
contentType = try values.decodeIfPresent(String.self, forKey: .contentType) ?? ""
fileType = try values.decodeIfPresent(FileType.self, forKey: .fileType) ?? FileType.unknown
parentId = 0
parentVersion = 0
maxWidth = 0
maxHeight = 0
maxPreviewSide = FileData.MAX_PREVIEW_SIDE
try super.init(from: decoder)
file = DiskFile(name: idFileName, live: true)
if isImage{
previewFile = DiskFile(name: previewFileName, live: true)
}
}
override public func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: ContentDataCodingKeys.self)
try container.encode(fileName, forKey: .fileName)
try container.encode(displayName, forKey: .displayName)
try container.encode(description, forKey: .description)
try container.encode(contentType, forKey: .contentType)
try container.encode(fileType, forKey: .fileType)
}
override public func copyFixedAttributes(from data: TypedData) {
super.copyFixedAttributes(from: data)
if let fileData = data as? FileData {
parentId = fileData.parentId
parentVersion = fileData.parentVersion
}
}
override public func copyEditableAttributes(from data: TypedData) {
super.copyEditableAttributes(from: data)
if let fileData = data as? FileData {
fileName = fileData.fileName
displayName = fileData.displayName
description = fileData.description
contentType = fileData.contentType
fileType = fileData.fileType
maxWidth = fileData.maxWidth
maxHeight = fileData.maxHeight
maxPreviewSide = fileData.maxPreviewSide
file = DiskFile(name: fileName, live: fileData.live)
if isImage{
previewFile = DiskFile(name: previewFileName, live: fileData.live)
}
}
}
override public func setCreateValues(request: Request) {
super.setCreateValues(request: request)
file = DiskFile(name: fileName, live: false)
if isImage{
previewFile = DiskFile(name: previewFileName, live: false)
}
}
override public func readRequest(_ request: Request) {
super.readRequest(request)
displayName = request.getString("displayName").trim()
description = request.getString("description")
if let memoryFile = request.getFile("file") {
print("has memory file")
fileName = memoryFile.name
contentType = memoryFile.contentType
fileType = FileType.fromContentType(contentType: contentType)
file = DiskFile(name: idFileName, live: false)
if !file.writeToDisk(memoryFile) {
request.addFormError("could not create file")
return;
}
if isImage {
if let memoryPreviewFile = memoryFile.createPreview(fileName: previewFileName, maxSize: FileData.MAX_PREVIEW_SIDE) {
previewFile = DiskFile(name: previewFileName, live: false)
if !previewFile!.writeToDisk(memoryPreviewFile) {
request.addFormError("could not create file")
return
}
}
}
if displayName.isEmpty {
displayName = fileName.pathWithoutExtension()
}
} else if isNew {
request.addIncompleteField("file")
}
}
public func moveTempFiles() -> Bool {
if !live {
Log.info("moving temp files")
file.makeLive()
if !file.live{
return false
}
if let pf = previewFile{
pf.makeLive()
}
}
return true
}
}

View File

@ -0,0 +1,29 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public enum FileType : String, Codable{
case unknown
case document
case image
case video
public static func fromContentType(contentType: String) -> FileType{
if (contentType.hasPrefix("document/") || contentType.contains("text") || contentType.contains("pdf")){
return .document
}
if (contentType.hasPrefix("image/")){
return .image
}
if (contentType.hasPrefix("video/")){
return .video
}
return .unknown
}
}

View File

@ -0,0 +1,38 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FullPageController : PageController{
public static let instance = FullPageController()
override public class var type : ControllerType{
get{
.fullpage
}
}
override public func showEditContent(contentData: ContentData, request: Request) -> Response {
if let cnt = request.getSessionContent(type: FullPageData.self) {
request.setContent(cnt)
}
request.addPageVar("url", "/ctrl/fullpage/saveContentData/\(contentData.id)")
setEditPageVars(contentData: contentData, request: request)
return ForwardResponse(page: "fullpage/editContentData.ajax", request: request)
}
override public func setEditPageVars(contentData: ContentData, request: Request) {
if let pageData = contentData as? FullPageData {
super.setEditPageVars(contentData: pageData, request: request)
request.addPageVar("cssClass", pageData.cssClass.toHtml())
}
}
}

View File

@ -0,0 +1,54 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
extension FullPageData{
public func getEditContentHtml(request: Request) -> String {
"""
<form action="/ctrl/{{type}}/savePage/{{id}}" method="post" id="pageform" name="pageform" accept-charset="UTF-8">
<div public class="btn-group btn-group-sm pageEditButtons">
<button type="submit" public class="btn btn-sm btn-success" onclick="updateEditor();">{{_savePage}}</button>
<button public class="btn btn-sm btn-secondary" onclick="return linkTo('/ctrl/{{type}}/cancelEditPage/{{id}}');">{{_cancel}}</button>
</div>
<div public class="{{css}}">
<div public class="ckeditField" id="content" contenteditable="true">{{content}}</div>
</div>
<input type="hidden" name="content" value="{{escapedContent}}" />
</form>
<script type="text/javascript">
$('#content').ckeditor({toolbar : 'Full',filebrowserBrowseUrl : '/ajax/ckeditor/openLinkBrowser/{{id}}',filebrowserImageBrowseUrl : '/ajax/ckeditor/openImageBrowser/{{id}}'});
public function updateEditor(){
if (CKEDITOR) {
$('input[name="content"]').val(CKEDITOR.instances['content'].getData());
}
}
</script>
""".format(language: request.language, [
"type": type.rawValue.toHtml(),
"id": String(id),
"css": cssClass,
"content": content,
"escapedContent": content.toHtml()
])
}
public func getDraftContentHtml(request: Request) -> String {
"""
<div public class="{{css}}">
{{content}}
</div>
""".format(language: request.language, [
"css": cssClass,
"content": content
])
}
}

View File

@ -0,0 +1,90 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FullPageData: PageData {
private enum TemplatePageDataCodingKeys: CodingKey {
case cssClass
case content
}
public var cssClass : String
public var content: String
override public var type: DataType {
get {
.fullpage
}
}
override init() {
cssClass = "paragraph"
content = ""
super.init()
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: TemplatePageDataCodingKeys.self)
cssClass = try values.decodeIfPresent(String.self, forKey: .cssClass) ?? ""
content = try values.decodeIfPresent(String.self, forKey: .content) ?? ""
try super.init(from: decoder)
}
override public func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: TemplatePageDataCodingKeys.self)
try container.encode(cssClass, forKey: .cssClass)
try container.encode(content, forKey: .content)
}
override public func copyEditableAttributes(from data: TypedData) {
super.copyEditableAttributes(from: data)
if let contentData = data as? FullPageData {
cssClass = contentData.cssClass
}
}
override public func copyPageAttributes(from data: ContentData) {
if let contentData = data as? FullPageData {
content = contentData.content
}
}
override public func createPublishedContent(request: Request) {
publishedContent = HtmlFormatter.format(src: """
<div public class="{{cssClass}}">
{{content}}
</div>
""".format(language: request.language, [
"cssClass": cssClass,
"content": content]), indented: false)
publishDate = Application.instance.currentTime
}
override public func readRequest(_ request: Request) {
super.readRequest(request)
cssClass = request.getString("cssClass")
}
override public func readPageRequest(_ request: Request) {
super.readPageRequest(request)
content = request.getString("content")
}
override public func displayEditContent(request: Request) -> String {
getEditContentHtml(request: request)
}
override public func displayDraftContent(request: Request) -> String {
getDraftContentHtml(request: request)
}
}

View File

@ -0,0 +1,78 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class PageController: ContentController {
override public func processRequest(method: String, id: Int?, request: Request) -> Response? {
if let response = super.processRequest(method: method, id: id, request: request) {
return response
}
switch method {
case "openEditPage": return openEditPage(id: id, request: request)
case "savePage": return savePage(id: id, request: request)
case "cancelEditPage": return cancelEditPage(id: id, request: request)
default:
return nil
}
}
public func openEditPage(id: Int?, request: Request) -> Response {
if let id = id, let original = ContentContainer.instance.getContent(id: id) as? PageData {
if !Right.hasUserEditRight(user: request.user, contentId: original.id) {
return Response(code: .forbidden)
}
if let data = DataFactory.create(type: original.type) as? PageData{
data.copyFixedAttributes(from: original)
data.copyEditableAttributes(from: original)
data.copyPageAttributes(from: original)
request.setSessionContent(data)
request.viewType = .edit
return show(id: data.id, request: request) ?? Response(code: .internalServerError)
}
}
return Response(code: .badRequest)
}
public func savePage(id: Int?, request: Request) -> Response {
if let id = id, let data = request.getSessionContent(type: PageData.self), data.id == id {
if !Right.hasUserEditRight(user: request.user, content: data) {
return Response(code: .forbidden)
}
data.readPageRequest(request)
if request.hasFormError {
request.viewType = .edit
return show(id: data.id, request: request) ?? Response(code: .internalServerError)
}
if !ContentContainer.instance.updateContent(data: data, userId: request.userId) {
Log.warn("original data not found for update")
request.setMessage("_versionError", type: .danger)
request.viewType = .edit
return show(id: data.id, request: request) ?? Response(code: .internalServerError)
}
request.removeSessionContent()
return show(id: id, request: request) ?? Response(code: .internalServerError)
}
return Response(code: .badRequest)
}
public func cancelEditPage(id: Int?, request: Request) -> Response {
if let id = id {
if !Right.hasUserReadRight(user: request.user, contentId: id) {
return Response(code: .forbidden)
}
request.removeSessionContent()
request.viewType = .show
return show(id: id, request: request) ?? Response(code: .internalServerError)
}
return Response(code: .badRequest)
}
}

View File

@ -0,0 +1,121 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class PageData: ContentData {
private enum PageDataCodingKeys: CodingKey {
case publishDate
case publishedContent
}
public var publishDate: Date?
public var publishedContent : String
override public var type: DataType {
get {
.page
}
}
override init() {
publishDate = nil
publishedContent = ""
super.init()
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: PageDataCodingKeys.self)
publishDate = try values.decodeIfPresent(Date?.self, forKey: .publishDate) ?? nil
publishedContent = try values.decodeIfPresent(String.self, forKey: .publishedContent) ?? ""
try super.init(from: decoder)
}
override public func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: PageDataCodingKeys.self)
try container.encode(publishDate, forKey: .publishDate)
try container.encode(publishedContent, forKey: .publishedContent)
}
override public func copyEditableAttributes(from data: TypedData) {
super.copyEditableAttributes(from: data)
if let contentData = data as? PageData {
publishDate = contentData.publishDate
publishedContent = contentData.publishedContent
}
}
public func copyPageAttributes(from data: ContentData) {
}
public func readPageRequest(_ request: Request) {
}
public func hasUnpublishedDraft() -> Bool {
if let pdate = publishDate {
return changeDate > pdate
}
return changeDate > creationDate
}
public func isPublished() -> Bool {
publishDate != nil
}
public func createPublishedContent(request: Request) {
}
override public func displayContent(request: Request) -> String {
var html = ""
switch request.viewType {
case .edit:
html.append("<div id=\"pageContent\" public class=\"editArea\">")
html.append(displayEditContent(request: request))
html.append("</div>")
case .showPublished:
html.append("<div id=\"pageContent\" public class=\"viewArea\">");
if isPublished() {
html.append(displayPublishedContent(request: request))
}
html.append("</div>")
case .showDraft:
html.append("<div id=\"pageContent\" public class=\"viewArea\">");
if Right.hasUserEditRight(user: request.user, contentId: id) {
html.append(displayDraftContent(request: request))
}
html.append("</div>")
case .show:
html.append("<div id=\"pageContent\" public class=\"viewArea\">")
if Right.hasUserReadRight(user: request.user, contentId: id) {
//Log.log("display draft");
html.append(displayDraftContent(request: request))
} else if (isPublished()) {
//Log.log("display published");
html.append(displayPublishedContent(request: request))
}
html.append("</div>")
}
return html
}
public func displayEditContent(request: Request) -> String {
""
}
public func displayDraftContent(request: Request) -> String {
""
}
public func displayPublishedContent(request: Request) -> String {
publishedContent
}
}

View File

@ -0,0 +1,61 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
extension Request{
public static let CONTENT_KEY = "$CONTENT"
public static let FILE_KEY = "$FILE"
public func setContent(_ content: ContentData){
setParam(Request.CONTENT_KEY, content)
}
public func getContent() -> ContentData?{
getParam(Request.CONTENT_KEY, type: ContentData.self)
}
public func getSafeContent() -> ContentData{
getParam(Request.CONTENT_KEY, type: ContentData.self) ?? ContentContainer.instance.contentRoot
}
public func getContent<T : ContentData>(type: T.Type) -> T?{
getParam(Request.CONTENT_KEY, type: type)
}
public func setSessionContent(_ content: ContentData){
setSessionAttribute(Request.CONTENT_KEY, value: content)
}
public func getSessionContent() -> ContentData?{
getSessionAttribute(Request.CONTENT_KEY, type: ContentData.self)
}
public func getSessionContent<T : ContentData>(type: T.Type) -> T?{
getSessionAttribute(Request.CONTENT_KEY, type: type)
}
public func removeSessionContent(){
removeSessionAttribute(Request.CONTENT_KEY)
}
public func setSessionFile(_ file: FileData){
setSessionAttribute(Request.FILE_KEY, value: file)
}
public func getSessionFile() -> FileData?{
getSessionAttribute(Request.FILE_KEY, type: FileData.self)
}
public func removeSessionFile(){
removeSessionAttribute(Request.FILE_KEY)
}
}

View File

@ -0,0 +1,74 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
extension Dictionary where Key : ExpressibleByStringLiteral, Value: PartField{
public func toItemDictionary() -> Dictionary<String, TypedFieldItem>{
var dict = Dictionary<String, TypedFieldItem>()
for key in keys{
if let k = key as? String, let v = self[key] {
dict[k] = TypedFieldItem(data: v)
}
}
return dict
}
}
extension Dictionary where Key : ExpressibleByStringLiteral, Value: TypedFieldItem{
public func toPartArray() -> Dictionary<String, PartField>{
var dict = Dictionary<String, PartField>()
for key in keys{
if let k = key as? String, let v = self[key] {
dict[k] = v.data
}
}
return dict
}
}
public class TypedFieldItem: Codable{
private enum CodingKeys: CodingKey{
case type
case data
}
public var type : DataType
public var data : PartField
init(data: PartField){
type = data.type
self.data = data
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
type = try values.decode(DataType.self, forKey: .type)
switch type{
case .textfield:
data = try values.decode(TextField.self, forKey: .data)
case .htmlfield:
data = try values.decode(HtmlField.self, forKey: .data)
default:
fatalError()
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type, forKey: .type)
try container.encode(data, forKey: .data)
}
}

View File

@ -0,0 +1,23 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class HtmlField : PartField{
override public var type : DataType{
get{
.htmlfield
}
}
override public func readRequest(_ request: Request) {
content = request.getString(identifier)
}
}

View File

@ -0,0 +1,70 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
extension Array where Element: PartData{
public func toItemArray() -> Array<TypedPartItem>{
var array = Array<TypedPartItem>()
for data in self{
array.append(TypedPartItem(data: data))
}
return array
}
}
extension Array where Element: TypedPartItem{
public func toPartArray() -> Array<PartData>{
var array = Array<PartData>()
for item in self{
array.append(item.data)
}
return array
}
}
public class TypedPartItem: Codable{
private enum CodingKeys: CodingKey{
case type
case data
}
public var type : DataType
public var data : PartData
init(data: PartData){
type = data.type
self.data = data
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
type = try values.decode(DataType.self, forKey: .type)
switch type {
case .part:
data = try values.decode(PartData.self, forKey: .data)
case .templatepart:
data = try values.decode(TemplatePartData.self, forKey: .data)
default:
fatalError()
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type, forKey: .type)
try container.encode(data, forKey: .data)
}
}

View File

@ -0,0 +1,67 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
extension PartData {
public func getEditPartHeader(request: Request) -> String {
var html = ""
let partId = id
html.append("""
<input type="hidden" name="{{positionName}}" value="{{value}}" public class="partPos"/>
<div public class="partEditButtons">
<div public class="btn-group btn-group-sm" role="group">
<div public class="btn-group btn-group-sm" role="group">
<button type="button" public class="btn btn-secondary fa fa-plus dropdown-toggle" data-toggle="dropdown" title="{{title}}"></button>
<div public class="dropdown-menu">
""".format(language: request.language, [
"positionName": partPositionName,
"value": String(position),
"title": "_newPart".toLocalizedHtml(language: request.language)]
))
if let templates = TemplateCache.getTemplates(type: TemplateType.part) {
for tpl in templates.values {
html.append("""
<a public class="dropdown-item" href="" onclick="return addPart({{partId}},'{{sectionName}}','{{partType}}','{{templateName}}');">
{{displayName}}
</a>
""".format(language: request.language, [
"partId": String(partId),
"sectionName": sectionName.toHtml(),
"partType": PartType.templatepart.rawValue.toHtml(),
"templateName": tpl.name.toHtml(),
"displayName": tpl.displayName.toHtml()]
))
}
}
html.append("""
</div>
</div>
<div public class="btn-group btn-group-sm" role="group">
<button type="button" public class="btn btn-secondary dropdown-toggle fa fa-ellipsis-h" data-toggle="dropdown" title="{{_more}}"></button>
<div public class="dropdown-menu">
<a public class="dropdown-item" href="" onclick="return movePart({{partId}},-1);">{{_up}}
</a>
<a public class="dropdown-item" href="" onclick="return movePart({{partId}},1);">{{_down}}
</a>
<a public class="dropdown-item" href="" onclick="if (confirmDelete()) return deletePart({{partId}});">{{_delete}}
</a>
</div>
</div>
</div>
</div>
""".format(language: request.language, [
"partId": String(partId)]
))
return html
}
}

View File

@ -0,0 +1,99 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class PartData: BaseData {
private enum PartDataCodingKeys: CodingKey {
case sectionName
case position
}
public var sectionName: String
public var position: Int
override public var type : DataType{
get {
.part
}
}
public var partType : PartType{
get {
.part
}
}
public var editTitle: String {
get {
"Section Part, ID=" + String(id)
}
}
public var partWrapperId: String {
get {
"part_" + String(id)
}
}
public var partPositionName: String {
get {
"partpos_" + String(id)
}
}
override init() {
sectionName = ""
position = 0
super.init()
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: PartDataCodingKeys.self)
sectionName = try values.decodeIfPresent(String.self, forKey: .sectionName) ?? ""
position = try values.decodeIfPresent(Int.self, forKey: .position) ?? 0
try super.init(from: decoder)
}
override public func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: PartDataCodingKeys.self)
try container.encode(sectionName, forKey: .sectionName)
try container.encode(position, forKey: .position)
}
override public func copyFixedAttributes(from data: TypedData) {
super.copyFixedAttributes(from: data)
if let partData = data as? PartData {
sectionName = partData.sectionName
}
}
override public func copyEditableAttributes(from data: TypedData) {
super.copyEditableAttributes(from: data)
if let partData = data as? PartData {
position = partData.position
}
}
override public func setCreateValues(request: Request) {
super.setCreateValues(request: request)
sectionName = request.getString("sectionName")
}
public func displayPart(request: Request) -> String {
""
}
public func getNewPartHtml(request: Request) -> String {
""
}
}

View File

@ -0,0 +1,81 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class PartField : TypedData, Codable{
public static var PARTFIELD_KEY = "partfield";
private enum PartFieldCodingKeys: CodingKey{
case id
case partId
case name
case content
}
public var type: DataType{
get{
.field
}
}
public var partId: Int
public var name: String
public var content: String
public var identifier: String {
get {
String(partId) + "_" + name
}
}
init(){
partId = 0
name = ""
content = ""
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: PartFieldCodingKeys.self)
partId = try values.decodeIfPresent(Int.self, forKey: .partId) ?? 0
if partId == 0 {
//fallback
partId = try values.decodeIfPresent(Int.self, forKey: .id) ?? 0
}
name = try values.decodeIfPresent(String.self, forKey: .name) ?? ""
content = try values.decodeIfPresent(String.self, forKey: .content) ?? ""
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: PartFieldCodingKeys.self)
try container.encode(partId, forKey: .partId)
try container.encode(name, forKey: .name)
try container.encode(content, forKey: .content)
}
public func getTypeKey() -> String{
PartField.PARTFIELD_KEY
}
public func copyFixedAttributes(from data: TypedData) {
}
public func copyEditableAttributes(from data: TypedData) {
if let partField = data as? PartField {
name = partField.name
content = partField.content
}
}
public func readRequest(_ request: Request) {
}
}

View File

@ -0,0 +1,23 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public enum PartType : String, Codable{
case part
case templatepart
public static func getNewPart(type: PartType) -> PartData?{
switch type{
case .templatepart: return TemplatePartData()
default: return nil
}
}
}

View File

@ -0,0 +1,46 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
extension Request{
public static let SECTION_KEY = "$SECTION"
public func setSection(_ section: SectionData){
setParam(Request.SECTION_KEY, section)
}
public func getSection() -> SectionData?{
getParam(Request.SECTION_KEY, type: SectionData.self)
}
public func removeSection(){
removeParam(Request.SECTION_KEY)
}
public static let PART_KEY = "$PART"
public func setPart(_ part: PartData){
setParam(Request.PART_KEY, part)
}
public func getPart() -> PartData?{
getParam(Request.PART_KEY, type: PartData.self)
}
public func getPart<T : PartData>(type: T.Type) -> T?{
getParam(Request.PART_KEY, type: type)
}
public func removePart(){
removeParam(Request.PART_KEY)
}
}

View File

@ -0,0 +1,134 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class SectionData : TypedData, Identifiable, Codable, Hashable{
public static func == (lhs: SectionData, rhs: SectionData) -> Bool {
lhs.name == rhs.name && lhs.contentId == rhs.contentId
}
private enum SectionDataCodingKeys: CodingKey{
case name
case contentId
case cssClass
case parts
}
public var name: String
public var contentId: Int
public var cssClass: String
public var parts: Array<PartData>
public var type : DataType{
get{
.section
}
}
public var sectionId: String {
get {
"section_" + name
}
}
init(){
name = ""
contentId = 0
cssClass = ""
parts = Array<PartData>()
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: SectionDataCodingKeys.self)
name = try values.decodeIfPresent(String.self, forKey: .name) ?? ""
contentId = try values.decodeIfPresent(Int.self, forKey: .contentId) ?? 0
cssClass = try values.decodeIfPresent(String.self, forKey: .cssClass) ?? ""
let items = try values.decodeIfPresent(Array<TypedPartItem>.self, forKey: .parts) ?? Array<TypedPartItem>()
parts = items.toPartArray()
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: SectionDataCodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(contentId, forKey: .contentId)
try container.encode(cssClass, forKey: .cssClass)
let items = parts.toItemArray()
try container.encode(items, forKey: .parts)
}
public func hash(into hasher: inout Hasher){
hasher.combine(name)
hasher.combine(contentId)
}
public func copyFixedAttributes(data: TypedData) {
if let sectionData = data as? SectionData {
name = sectionData.name
contentId = sectionData.contentId
cssClass = sectionData.cssClass
}
}
public func copyEditableAttributes(data: TypedData) {
if let sectionData = data as? SectionData {
parts.removeAll()
for part in sectionData.parts {
if let newPart = PartType.getNewPart(type: part.partType) {
newPart.copyFixedAttributes(from: part)
newPart.copyEditableAttributes(from: part)
parts.append(newPart)
}
}
}
}
public func readRequest(_ request: Request) {
for part in parts.reversed(){
part.readRequest(request)
//marker for removed part
if part.position == -1 {
parts.remove(obj: part)
}
}
sortParts()
}
public func sortParts(){
parts.sort(by: {lhs, rhs in
lhs.position < rhs.position
})
}
public func addPart(part: PartData, fromPartId : Int) {
var found = false
if fromPartId != -1 {
for i in 0..<parts.count{
let ppd = parts[i]
if ppd.id == fromPartId {
parts.insert(part, at: i+1)
found = true;
break
}
}
}
if (!found) {
parts.append(part)
}
setRankings()
}
public func setRankings() {
for i in 0..<parts.count {
parts[i].position = i + 1
}
}
}

View File

@ -0,0 +1,78 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class TemplatePageController: PageController {
public static let instance = TemplatePageController()
override public class var type: ControllerType {
get {
.templatepage
}
}
override public func processRequest(method: String, id: Int?, request: Request) -> Response? {
if let response = super.processRequest(method: method, id: id, request: request) {
return response
}
switch method {
case "addPart": return addPart(id: id, request: request)
default:
return nil
}
}
public func addPart(id: Int?, request: Request) -> Response {
if let data = request.getSessionContent(type: TemplatePageData.self) , id == data.id {
if Right.hasUserEditRight(user: request.user, contentId: data.id) {
let fromPartId = request.getInt("fromPartId", def: -1)
if let partType = PartType(rawValue: request.getString("partType")) {
if let pdata = PartType.getNewPart(type: partType) {
pdata.setCreateValues(request: request)
data.addPart(part: pdata, fromPartId: fromPartId)
request.setSessionAttribute("part", value: pdata)
request.setContent(data)
return Response(html: pdata.getNewPartHtml(request: request))
}
}
}
}
return Response(code: .badRequest)
}
override public func showEditContent(contentData: ContentData, request: Request) -> Response {
if let cnt = request.getSessionContent(type: TemplatePageData.self){
request.setContent(cnt)
}
request.addPageVar("url", "/ctrl/templatepage/saveContentData/\(contentData.id)")
setEditPageVars(contentData: contentData, request: request)
return ForwardResponse(page: "templatepage/editContentData.ajax", request: request)
}
override public func setEditPageVars(contentData: ContentData, request: Request) {
super.setEditPageVars(contentData: contentData, request: request)
if let pageData = contentData as? TemplatePageData {
var str = """
<option value="" "\(pageData.template.isEmpty ? "selected" : "") > \("_pleaseSelect".toLocalizedHtml(language: request.language)) </option>
"""
if let templates = TemplateCache.getTemplates(type: .page) {
for tpl in templates.keys {
str.append(
"""
<option value="\(tpl.toHtml())" " \(pageData.template == tpl ? "selected" : "")>\(tpl.toHtml()) </option>
""")
}
}
request.addPageVar("templateOptions", str)
}
}
}

View File

@ -0,0 +1,135 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class TemplatePageData : PageData{
private enum TemplatePageDataCodingKeys: CodingKey{
case template
case sections
}
public var template : String
public var sections : Dictionary<String, SectionData>
override public var type : DataType{
get {
.templatepage
}
}
override init(){
template = ""
sections = Dictionary<String, SectionData>()
super.init()
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: TemplatePageDataCodingKeys.self)
template = try values.decodeIfPresent(String.self, forKey: .template) ?? ""
sections = try values.decodeIfPresent(Dictionary<String, SectionData>.self, forKey: .sections) ?? Dictionary<String, SectionData>()
try super.init(from: decoder)
}
override public func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: TemplatePageDataCodingKeys.self)
try container.encode(template, forKey: .template)
try container.encode(sections, forKey: .sections)
}
override public func copyEditableAttributes(from data: TypedData) {
super.copyEditableAttributes(from: data)
if let contentData = data as? TemplatePageData{
template = contentData.template
sections.removeAll()
sections.addAll(from: contentData.sections)
}
}
override public func copyPageAttributes(from data: ContentData) {
sections.removeAll()
if let contentData = data as? TemplatePageData {
for sectionName in contentData.sections.keys {
if let section = contentData.sections[sectionName] {
if let newSection = DataFactory.create(type: .section) as? SectionData{
newSection.copyFixedAttributes(data: section)
newSection.copyEditableAttributes(data: section)
sections[sectionName] = newSection
}
}
}
}
}
public func ensureSection(sectionName: String) -> SectionData {
if sections.keys.contains(sectionName) {
return sections[sectionName]!
}
let section = SectionData()
section.contentId = id
section.name = sectionName
sections[sectionName] = section
return section
}
override public func createPublishedContent(request: Request) {
request.setSessionContent(self)
request.setContent(self)
publishedContent = HtmlFormatter.format(src: getTemplateHtml(request: request), indented: false)
publishDate = Application.instance.currentTime
request.removeSessionContent()
}
override public func readRequest(_ request: Request) {
super.readRequest(request);
template = request.getString("template")
if template.isEmpty{
request.addIncompleteField("template")
}
}
override public func readPageRequest(_ request: Request) {
super.readPageRequest(request);
for section in sections.values {
section.readRequest(request);
}
}
// part data
public func addPart(part: PartData, fromPartId: Int) {
let section = ensureSection(sectionName: part.sectionName)
section.addPart(part: part, fromPartId: fromPartId)
}
override public func displayEditContent(request: Request) -> String {
request.addPageVar("type", type.rawValue)
request.addPageVar("id", String(id))
request.addPageVar("template", getTemplateHtml(request: request))
return ServerPageController.processPage(path: "templatepage/editPageContent.inc", request: request) ?? ""
}
public func getTemplateHtml(request: Request) -> String{
if let tpl = TemplateCache.getTemplate(type: TemplateType.page, name: template){
return tpl.getHtml(request: request)
}
return ""
}
override public func displayDraftContent(request: Request) -> String {
getTemplateHtml(request: request)
}
override public func displayPublishedContent(request: Request) -> String {
publishedContent
}
}

View File

@ -0,0 +1,147 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class TemplatePartData: PartData {
private enum TemplatePartDataCodingKeys: CodingKey {
case template
case fields
}
public var template: String
public var fields: Dictionary<String, PartField>
override public var type: DataType {
get {
.templatepart
}
}
override public var partType: PartType {
get {
.templatepart
}
}
override init() {
template = ""
fields = Dictionary<String, PartField>()
super.init()
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: TemplatePartDataCodingKeys.self)
template = try values.decodeIfPresent(String.self, forKey: .template) ?? ""
let dict = try values.decodeIfPresent(Dictionary<String, TypedFieldItem>.self, forKey: .fields) ?? Dictionary<String, TypedFieldItem>()
fields = dict.toPartArray()
try super.init(from: decoder)
}
override public func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: TemplatePartDataCodingKeys.self)
try container.encode(template, forKey: .template)
let dict = fields.toItemDictionary()
try container.encode(dict, forKey: .fields)
}
override public func copyEditableAttributes(from data: TypedData) {
super.copyEditableAttributes(from: data)
if let partData = data as? TemplatePartData {
template = partData.template
fields.removeAll()
for key in partData.fields.keys {
if let field = partData.fields[key] {
if let newField = DataFactory.create(type: field.type) as? PartField {
newField.copyFixedAttributes(from: field)
newField.copyEditableAttributes(from: field)
fields[key] = newField
}
}
}
}
}
override public func readRequest(_ request: Request) {
position = request.getInt(partPositionName, def: -1)
for field in fields.values {
field.readRequest(request)
}
}
override public func setCreateValues(request: Request) {
super.setCreateValues(request: request)
template = request.getString("template")
}
public func ensureTextField(name: String) -> TextField? {
if let field = fields[name] as? TextField {
return field
}
let textfield = TextField()
textfield.name = name
textfield.partId = id
fields[name] = textfield
return textfield
}
public func ensureHtmlField(name: String) -> HtmlField {
if let field = fields[name] as? HtmlField {
return field
}
let htmlfield = HtmlField()
htmlfield.name = name
htmlfield.partId = id
fields[name] = htmlfield
return htmlfield
}
override public func displayPart(request: Request) -> String {
var html = ""
if let partTemplate = TemplateCache.getTemplate(type: TemplateType.part, name: template) {
request.setPart(self)
if request.viewType == ViewType.edit {
html.append("""
<div id="{{wrapperId}}" public class="partWrapper {{css}}" title="{{title}}">
""".format(language: request.language, [
"wrapperId": partWrapperId,
"css": partTemplate.css.toHtml(),
"title": editTitle.toHtml()]
))
html.append(getEditPartHeader(request: request))
} else {
html.append("""
<div id="{{wrapperId}}" public class="partWrapper {{css}}">
""".format(language: request.language, [
"wrapperId": partWrapperId,
"css": partTemplate.css.toHtml()]
))
}
html.append(partTemplate.getHtml(request: request))
html.append("</div>")
request.removePart()
}
return html
}
override public func getNewPartHtml(request: Request) -> String {
request.viewType = .edit
var html = displayPart(request: request)
html.append("""
<script type="text/javascript">
updatePartEditors($('#{{wrapperId}}'));
</script>
""".format(language: request.language, [
"wrapperId": partWrapperId]))
return html
}
}

View File

@ -0,0 +1,24 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class TextField : PartField{
override public var type : DataType{
get{
.textfield
}
}
override public func readRequest(_ request: Request) {
content = request.getString(identifier)
}
}

View File

@ -0,0 +1,17 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public enum ViewType: String {
case show
case showDraft
case showPublished
case edit
}

View File

@ -0,0 +1,122 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class BaseData: TypedData, Identifiable, Codable, Hashable{
public static func == (lhs: BaseData, rhs: BaseData) -> Bool {
lhs.id == rhs.id && lhs.version == rhs.version
}
private enum BaseDataCodingKeys: CodingKey{
case id
case version
case creationDate
case changeDate
case creatorId
case changerId
}
public var id: Int
public var version: Int
public var creationDate: Date
public var changeDate: Date
public var creatorId: Int
public var changerId: Int
public var isNew = false
public var type : DataType{
get {
.base
}
}
init(){
isNew = false
id = 0
version = 1
creationDate = Date()
changeDate = Date()
creatorId = 1
changerId = 1
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: BaseDataCodingKeys.self)
id = try values.decodeIfPresent(Int.self, forKey: .id) ?? 0
version = try values.decodeIfPresent(Int.self, forKey: .version) ?? 1
creationDate = try values.decodeIfPresent(Date.self, forKey: .creationDate) ?? Date()
changeDate = try values.decodeIfPresent(Date.self, forKey: .changeDate) ?? Date()
creatorId = try values.decodeIfPresent(Int.self, forKey: .creatorId) ?? 1
changerId = try values.decodeIfPresent(Int.self, forKey: .changerId) ?? 1
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: BaseDataCodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(version, forKey: .version)
try container.encode(creationDate, forKey: .creationDate)
try container.encode(changeDate, forKey: .changeDate)
try container.encode(creatorId, forKey: .creatorId)
try container.encode(changerId, forKey: .changerId)
}
public func hash(into hasher: inout Hasher){
hasher.combine(id)
hasher.combine(version)
}
public func copyFixedAttributes(from data: TypedData) {
if let baseData = data as? BaseData {
id = baseData.id
creationDate = baseData.creationDate
creatorId = baseData.creatorId
}
}
public func copyEditableAttributes(from data: TypedData) {
if let baseData = data as? BaseData {
version = baseData.version
changeDate = baseData.changeDate
changerId = baseData.changerId
}
}
public func increaseVersion() {
version += 1;
}
public func readRequest(_ request: Request) {
}
public func setCreateValues(request: Request) {
isNew = true
id = IdService.instance.getNextId()
version = 1
creatorId = request.userId
creationDate = Application.instance.currentTime
changerId = request.userId
changeDate = creationDate
}
public func isEqualByIdAndVersion(data: BaseData) -> Bool{
id == data.id && version == data.version
}
public func prepareDelete(){
}
public func isComplete() -> Bool{
true
}
}

View File

@ -0,0 +1,33 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public struct Clipboard {
public static var instance = Clipboard()
private var dataDict = Dictionary<DataType, BaseData>()
public func hasData(type: DataType) -> Bool{
dataDict.keys.contains(type)
}
public func getData(type: DataType) -> BaseData?{
dataDict[type]
}
mutating public func setData(type: DataType, data: BaseData){
dataDict[type] = data
}
mutating public func removeData(type: DataType){
dataDict[type] = nil
}
}

View File

@ -0,0 +1,63 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class DataContainer : Codable{
private enum DataContainerCodingKeys: CodingKey{
case changeDate
case version
}
public var changeDate: Date
public var version: Int
public var changed = false
public required init(){
changeDate = Date()
version = 1
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: DataContainerCodingKeys.self)
changeDate = try values.decodeIfPresent(Date.self, forKey: .changeDate) ?? Date()
version = try values.decodeIfPresent(Int.self, forKey: .version) ?? 1
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: DataContainerCodingKeys.self)
try container.encode(changeDate, forKey: .changeDate)
try container.encode(version, forKey: .version)
}
public func increaseVersion() {
version += 1;
}
public func checkChanged(){
fatalError("not implemented")
}
public func setHasChanged() {
if (!changed) {
increaseVersion();
changeDate = Date()
changed = true;
CheckDataAction.addToQueue()
}
}
public func save() -> Bool{
fatalError("not implemented")
}
}

View File

@ -0,0 +1,32 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public struct DataFactory{
public static func create(type: DataType) -> TypedData{
switch type {
case .base: return BaseData()
case .content: return ContentData()
case .page: return PageData()
case .fullpage: return FullPageData()
case .templatepage: return TemplatePageData()
case .section: return SectionData()
case .part: return PartData()
case .templatepart: return TemplatePartData()
case .field: return PartField()
case .htmlfield: return HtmlField()
case .textfield: return TextField()
case .file: return FileData()
case .group: return GroupData()
case .user: return UserData()
}
}
}

View File

@ -0,0 +1,54 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class DiskFile{
public var name: String
public var live: Bool
public var path: String{
get{
(live ? Paths.fileDirectory : Paths.tempFileDirectory).appendPath(name)
}
}
init(){
name = ""
live = false
}
init(name: String, live: Bool){
self.name = name
self.live = live
}
public func exists() -> Bool{
Files.fileExists(path: path)
}
public func writeToDisk(_ memoryFile: MemoryFile) -> Bool{
if Files.fileExists(path: path){
_ = Files.deleteFile(path: path)
}
return Files.saveFile(data: memoryFile.data, path: path)
}
public func makeLive(){
if !live{
let tmpPath = path
live = true
if !Files.moveFile(from: tmpPath, to: path){
Log.error("could not move file to live")
live = false
}
}
}
}

View File

@ -0,0 +1,66 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class IdService {
static public var instance = IdService()
public static func initialize(){
//id
if let s : String = Files.readTextFile(path: Paths.nextIdFile){
instance.nextId = Int(s) ?? 1000
}
else{
_ = Files.saveFile(text: String(instance.nextId), path: Paths.nextIdFile)
}
instance.idChanged = false;
}
private var nextId : Int = 1000
private var idChanged = false
private let idSemaphore = DispatchSemaphore(value: 1)
private func lockId(){
idSemaphore.wait()
}
private func unlockId(){
idSemaphore.signal()
}
public func getNextId() -> Int{
lockId()
defer{unlockId()}
idChanged = true;
nextId += 1
return nextId
}
public func checkIdChanged() {
lockId()
defer{unlockId()}
if (idChanged) {
if !saveId(){
Log.error("could not save id")
}
idChanged = false
}
}
private func saveId() -> Bool{
Files.saveFile(text: String(nextId), path: Paths.nextIdFile)
}
}

View File

@ -0,0 +1,47 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
#if os(macOS)
import Cocoa
#elseif os(Linux)
import SwiftGD
#endif
public class MemoryFile{
public var name : String
public var data : Data
public var contentType : String
init(name: String, data: Data){
self.name = name
self.data = data
contentType = MimeType.from(name)
}
public func createPreview(fileName: String, maxSize: Int) -> MemoryFile?{
#if os(macOS)
if let src = NSImage(data: data){
if let previewImage : NSImage = src.resizeMaintainingAspectRatio(withSize: NSSize(width: FileData.MAX_PREVIEW_SIDE, height: FileData.MAX_PREVIEW_SIDE)){
if let tiff = previewImage.tiffRepresentation, let tiffData = NSBitmapImageRep(data: tiff) {
if let previewData = tiffData.representation(using: .jpeg, properties: [:]) {
let preview = MemoryFile(name: fileName, data: previewData)
preview.contentType = "image/jpeg"
return preview
}
}
}
}
#elseif os(Linux)
#endif
return nil
}
}

View File

@ -0,0 +1,70 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
adapted from Wikimedia
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
import Foundation
public struct MimeType {
public static let DEFAULT_MIME_TYPE = "application/octet-stream"
public static let mimeTypes = [
"html": "text/html",
"htm": "text/html",
"shtml": "text/html",
"css": "text/css",
"xml": "text/xml",
"gif": "image/gif",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"js": "application/javascript",
"txt": "text/plain",
"png": "image/png",
"ico": "image/x-icon",
"bmp": "image/x-ms-bmp",
"svg": "image/svg+xml",
"svgz": "image/svg+xml",
"json": "application/json",
"doc": "application/msword",
"pdf": "application/pdf",
"ps": "application/postscript",
"eps": "application/postscript",
"ai": "application/postscript",
"rtf": "application/rtf",
"xls": "application/vnd.ms-excel",
"ppt": "application/vnd.ms-powerpoint",
"7z": "application/x-7z-compressed",
"rar": "application/x-rar-compressed",
"swf": "application/x-shockwave-flash",
"xhtml": "application/xhtml+xml",
"zip": "application/zip",
"img": "application/octet-stream",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"mid": "audio/midi",
"midi": "audio/midi",
"kar": "audio/midi",
"mp3": "audio/mpeg",
"ogg": "audio/ogg",
"m4a": "audio/x-m4a",
"mp4": "video/mp4",
"mpeg": "video/mpeg",
"mpg": "video/mpeg",
"mov": "video/quicktime",
"webm": "video/webm"
]
public static func from(_ path: String) -> String {
let ext = NSString(string: path).pathExtension
return MimeType.mimeTypes[ext.lowercased()] ?? MimeType.DEFAULT_MIME_TYPE
}
}

View File

@ -0,0 +1,96 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class Statics: Codable{
public static var instance = Statics()
public static var title = "Swifty Bandika"
public static var startSize = NSMakeSize(1000, 750)
public static func initialize(){
Log.info("initializing statics")
if !Files.fileExists(path: Paths.staticsFile){
instance.initDefaults()
if !instance.save(){
Log.error("could not save statics")
}
else {
Log.info("created statics")
}
}
if let str = Files.readTextFile(path: Paths.staticsFile){
if let statics : Statics = Statics.fromJSON(encoded: str){
instance = statics
Log.info("loaded app statics")
if !statics.save(){
Log.warn("statics could not be saved")
}
}
}
}
private enum SectionDataCodingKeys: CodingKey{
case salt
case defaultPassword
case defaultLocale
case cleanupInterval
case shutdownCode
}
public var salt: String
public var defaultPassword: String
public var defaultLocale: Locale
public var cleanupInterval : Int = 10
public var shutdownCode : String
public required init(){
salt = ""
defaultPassword = ""
defaultLocale = Locale(identifier: "en")
shutdownCode = ""
}
public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: SectionDataCodingKeys.self)
salt = try values.decodeIfPresent(String.self, forKey: .salt) ?? UserSecurity.generateSaltString()
defaultPassword = try values.decodeIfPresent(String.self, forKey: .defaultPassword) ?? ""
defaultLocale = try values.decodeIfPresent(Locale.self, forKey: .defaultLocale) ?? Locale.init(identifier: "en")
cleanupInterval = try values.decodeIfPresent(Int.self, forKey: .cleanupInterval) ?? 10
shutdownCode = try values.decodeIfPresent(String.self, forKey: .shutdownCode) ?? UserSecurity.generateShutdownString()
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: SectionDataCodingKeys.self)
try container.encode(salt, forKey: .salt)
try container.encode(defaultPassword, forKey: .defaultPassword)
try container.encode(defaultLocale, forKey: .defaultLocale)
try container.encode(shutdownCode, forKey: .shutdownCode)
}
public func initDefaults(){
salt = UserSecurity.generateSaltString()
defaultPassword = UserSecurity.encryptPassword(password: "pass", salt: salt)
defaultLocale = Locale(identifier: "en")
shutdownCode = UserSecurity.generateShutdownString()
}
public func save() -> Bool{
Log.info("saving app statics")
let json = toJSON()
if !Files.saveFile(text: json, path: Paths.staticsFile){
Log.warn("\(Paths.staticsFile) not saved")
return false
}
return true
}
}

View File

@ -0,0 +1,35 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public enum DataType : String, Codable{
case base
case content
case page
case fullpage
case templatepage
case part
case templatepart
case section
case field
case htmlfield
case textfield
case file
case group
case user
}
public protocol TypedData{
var type : DataType{
get
}
}

View File

@ -0,0 +1,84 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public enum Right : Int, Codable{
case NONE
case READ
case EDIT
case APPROVE
case FULL
public func includesRight(right: Right) -> Bool{
self.rawValue >= right.rawValue
}
public static func hasUserReadRight(user: UserData?, contentId : Int) -> Bool{
if let data = ContentContainer.instance.getContent(id: contentId){
if data.openAccess{
return true
}
return hasUserReadRight(user: user, content: data)
}
return false
}
public static func hasUserReadRight(user: UserData?, content: ContentData) -> Bool{
SystemZone.hasUserSystemRight(user: user,zone: SystemZone.contentRead) ||
content.openAccess ||
hasUserGroupRight(user: user, data: content, right: Right.READ)
}
public static func hasUserEditRight(user: UserData?, contentId: Int) -> Bool{
if let data = ContentContainer.instance.getContent(id: contentId){
return hasUserEditRight(user: user, content: data)
}
return false
}
public static func hasUserEditRight(user: UserData?, content: ContentData) -> Bool{
SystemZone.hasUserSystemRight(user: user,zone: SystemZone.contentEdit) ||
hasUserGroupRight(user: user, data: content, right: Right.EDIT)
}
public static func hasUserApproveRight(user: UserData?, contentId: Int) -> Bool{
if let data = ContentContainer.instance.getContent(id: contentId){
return hasUserApproveRight(user: user, content: data)
}
return false
}
public static func hasUserApproveRight(user: UserData?, content: ContentData) -> Bool{
SystemZone.hasUserSystemRight(user: user,zone: SystemZone.contentApprove) ||
hasUserGroupRight(user: user, data: content, right: Right.APPROVE)
}
public static func hasUserAnyGroupRight(user: UserData?, data: ContentData?) -> Bool{
if let user = user, let data = data{
for groupId in data.groupRights.keys{
if user.groupIds.contains(groupId) && !data.groupRights.isEmpty{
return true
}
}
}
return false
}
public static func hasUserGroupRight(user: UserData?, data: ContentData?, right: Right) -> Bool{
if let user = user, let data = data{
for groupId in data.groupRights.keys{
if user.groupIds.contains(groupId) && data.groupRights[groupId]!.includesRight(right: right){
return true
}
}
}
return false
}
}

View File

@ -0,0 +1,49 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public enum SystemZone : String, Codable, CaseIterable{
case user
case contentRead
case contentEdit
case contentApprove
public static func hasUserAnySystemRight(user: UserData?) -> Bool {
if let data = user{
if data.isRoot {
return true
}
for groupId in data.groupIds{
if let group = UserContainer.instance.getGroup(id: groupId){
if !group.systemRights.isEmpty{
return true
}
}
}
}
return false
}
public static func hasUserSystemRight(user: UserData?, zone: SystemZone) -> Bool{
if let data = user{
if data.isRoot {
return true
}
for groupId in data.groupIds {
if let group = UserContainer.instance.getGroup(id: groupId){
if group.systemRights.contains(zone){
return true
}
}
}
}
return false
}
}

View File

@ -0,0 +1,29 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class CloseDialogResponse: Response{
init(url: String, request: Request){
request.addPageVar("url", url)
request.addPageVar("hasMessage", String(request.hasMessage))
if request.hasMessage{
request.addPageVar("message", request.message!.text)
request.addPageVar("messageType", request.message!.type.rawValue)
}
if let html = ServerPageController.processPage(path: "closeDialog.ajax", request: request) {
super.init(html: html)
}
else {
super.init(code: .internalServerError)
}
}
}

View File

@ -0,0 +1,71 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public enum ControllerType: String {
case admin
case ckeditor
case file
case fullpage
case group
case templatepage
case user
}
public class Controller {
public class var type: ControllerType {
get {
fatalError()
}
}
public func processRequest(method: String, id: Int?, request: Request) -> Response? {
nil
}
public func setSuccess(request: Request, _ name: String) {
request.setMessage(name.toLocalizedHtml(language: request.language), type: .success)
}
public func setError(request: Request, _ name: String) {
request.setMessage(name.toLocalizedHtml(language: request.language), type: .danger)
}
public func showHome(request: Request) -> Response{
let home = ContentContainer.instance.contentRoot
if let controller = ControllerFactory.getDataController(type: home.type) as? ContentController{
return controller.show(id: home.id, request: request) ?? Response(code: .notFound)
}
return Response(code: .notFound)
}
public func openAdminPage(page: String, request: Request) -> Response {
request.addPageVar("title", (Configuration.instance.applicationName + " | " + "_administration".localize(language: request.language)).toHtml())
request.addPageVar("includeUrl", page)
request.addPageVar("hasUserRights", String(SystemZone.hasUserSystemRight(user: request.user, zone: .user)))
request.addPageVar("hasContentRights", String(SystemZone.hasUserSystemRight(user: request.user, zone: .contentEdit)))
return ForwardResponse(page: "administration/adminMaster", request: request)
}
public func showUserAdministration(request: Request) -> Response {
openAdminPage(page: "administration/userAdministration", request: request)
}
public func showContentAdministration(request: Request) -> Response {
return openAdminPage(page: "administration/contentAdministration", request: request)
}
public func showContentAdministration(contentId: Int, request: Request) -> Response {
request.setParam("contentId", contentId)
return showContentAdministration(request: request)
}
}

View File

@ -0,0 +1,34 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public struct ControllerFactory {
public static func getController(type: ControllerType) -> Controller? {
switch type {
case .admin: return AdminController.instance
case .ckeditor: return CkEditorController.instance
case .file: return FileController.instance
case .fullpage: return FullPageController.instance
case .group: return GroupController.instance
case .templatepage: return TemplatePageController.instance
case .user: return UserController.instance
}
}
public static func getDataController(type: DataType) -> Controller?{
switch type{
case .fullpage: return FullPageController.instance
case .templatepage: return TemplatePageController.instance
default: return nil
}
}
}

View File

@ -0,0 +1,55 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FormError {
public var formErrors = Array<String>()
public var formFields = Set<String>()
public var formIncomplete = false
public var isEmpty : Bool {
get{
formErrors.isEmpty && formFields.isEmpty
}
}
public func addFormError(_ s: String) {
if !formErrors.contains(s) {
formErrors.append(s)
}
}
public func addFormField(_ field: String) {
formFields.insert(field)
}
public func getFormErrorString() -> String {
if formErrors.isEmpty || formErrors.isEmpty {
return ""
}
if formErrors.count == 1 {
return formErrors[0]
}
var s = ""
for formError in formErrors {
if s.count > 0 {
s.append("\n")
}
s.append(formError)
}
return s
}
public func hasFormErrorField(name: String) -> Bool {
formFields.contains(name)
}
}

View File

@ -0,0 +1,23 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class ForwardResponse: Response{
init(page: String, request: Request){
if let html = ServerPageController.processPage(path: page, request: request) {
super.init(html: html)
}
else {
super.init(code: .internalServerError)
}
}
}

View File

@ -0,0 +1,43 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
import NIO
import NIOHTTP1
final public class HTTPHandler : ChannelInboundHandler {
public typealias InboundIn = HTTPServerRequestPart
let router : Router
public var request = Request()
init(router: Router) {
self.router = router
}
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let reqPart = unwrapInboundIn(data)
switch reqPart {
case .head(let header):
//Log.info(header)
request.readHeader(header)
case .body(var body):
request.appendBody(&body)
case .end:
request.readBody()
request.setSession()
//Log.info("session user is \(request.session?.user?.login ?? "none")")
if let response = router.route(request){
response.sessionId = request.session?.sessionId
response.process(channel: context.channel)
}
}
}
}

View File

@ -0,0 +1,76 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
import NIO
import NIOHTTP1
public protocol HttpServerStateDelegate{
func serverStateChanged()
}
public class HttpServer{
public static var instance = HttpServer()
public var loopGroup : MultiThreadedEventLoopGroup? = nil
public var serverChannel : Channel? = nil
public var operating = false
public var router = Router()
public var delegate : HttpServerStateDelegate? = nil
public func start() {
loopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let reuseAddrOpt = ChannelOptions.socket(
SocketOptionLevel(SOL_SOCKET),
SO_REUSEADDR)
let bootstrap = ServerBootstrap(group: loopGroup!)
.serverChannelOption(ChannelOptions.backlog, value: 256)
.serverChannelOption(reuseAddrOpt, value: 1)
.childChannelInitializer { channel in
channel.pipeline.configureHTTPServerPipeline().flatMap {
channel.pipeline.addHandler(HTTPHandler(router: self.router))
}
}
.childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
.childChannelOption(reuseAddrOpt, value: 1)
.childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1)
do {
serverChannel = try bootstrap.bind(host: Configuration.instance.host, port: Configuration.instance.webPort)
.wait()
operating = true
delegate?.serverStateChanged()
Log.info("Server has started as \(Configuration.instance.host) on port \(Configuration.instance.webPort)")
try serverChannel!.closeFuture.wait()
}
catch {
Log.error("failed to start server: \(error)")
}
}
public func stop(){
Log.info("Shutting down Server")
do {
if serverChannel != nil{
serverChannel!.close(mode: .all, promise: nil)
}
try loopGroup?.syncShutdownGracefully()
operating = false
Log.info("Server has stopped")
loopGroup = nil
delegate?.serverStateChanged()
} catch {
Log.error("Shutting down server failed: \(error)")
}
}
}

View File

@ -0,0 +1,42 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class Message {
public var type: MessageType
public var text: String
init(type: MessageType, text: String){
self.type = type
self.text = text
}
public static var messageHtml =
"""
<div public class="alert alert-{{type}} alert-dismissible fade show" role="alert">
{{message}}
<button type="button" public class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
"""
public func getHtml(request: Request) -> String {
if request.hasMessage {
return Message.messageHtml.format(language: request.language, [
"type": type.rawValue,
"message": text.hasPrefix("_") ? text.toLocalizedHtml(language: request.language) : text.toHtml()]
)
}
return ""
}
}

View File

@ -0,0 +1,16 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public enum MessageType : String, Codable{
case info
case success
case danger
}

View File

@ -0,0 +1,86 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
import NIO
import NIOHTTP1
extension Request{
public func readHeader(_ header: HTTPRequestHead){
uri = header.uri
if let idx = uri.firstIndex(of: "?"){
path = String(uri[uri.startIndex..<idx])
}
else{
path = uri
}
if let queryItems = URLComponents(string: header.uri)?.queryItems{
params.addAll(from: Dictionary(grouping: queryItems, by: { $0.name }).mapValues { $0.compactMap({ $0.value }).joined(separator: ",") })
}
method = header.method
headers = Dictionary(grouping: header.headers, by: { $0.name.lowercased() }).mapValues { $0.compactMap({ $0.value }).joined(separator: ",") }
setLanguage()
}
public func setSession(){
session = SessionCache.getSession(sessionId: sessionId)
}
public func appendBody(_ body: inout ByteBuffer){
if let newBytes = body.readBytes(length: body.readableBytes){
//print("append \(newBytes.count) bytes")
bytes.append(contentsOf: newBytes)
}
}
public func readBody(){
switch contentType{
case "application/x-www-form-urlencoded":
parseUrlencodedForm()
case "multipart/form-data":
parseMultiPartFormData()
default:
break
}
}
public func hasTokenForHeader(_ headerName: String, token: String) -> Bool {
guard let headerValue = headers[headerName] else {
return false
}
return headerValue.components(separatedBy: ",").filter({ $0.trimmingCharacters(in: .whitespaces).lowercased() == token }).count > 0
}
public func parseUrlencodedForm() {
guard let utf8String = String(bytes: bytes, encoding: .utf8) else {
return
}
guard contentType == "application/x-www-form-urlencoded" else {
return
}
for param in utf8String.components(separatedBy: "&"){
let tokens = param.components(separatedBy: "=")
if var name = tokens.first?.removingPercentEncoding, var value = tokens.last?.removingPercentEncoding, tokens.count == 2 {
name = name.replacingOccurrences(of: "+", with: " ")
value = value.replacingOccurrences(of: "+", with: " ")
if var array = params[name] as? Array<String>{
array.append(value)
params[name] = array
}
else{
var array = Array<String>()
array.append(value)
params[name] = array
}
}
}
}
}

View File

@ -0,0 +1,153 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
extension Request{
public struct MultiPart {
public let headers: [String: String]
public let body: [UInt8]
public var name: String? {
valueFor("content-disposition", parameter: "name")?.unquote()
}
public var fileName: String? {
valueFor("content-disposition", parameter: "filename")?.unquote()
}
private func valueFor(_ headerName: String, parameter: String) -> String? {
headers.reduce([String]()) { (combined, header: (key: String, value: String)) -> [String] in
guard header.key == headerName else {
return combined
}
let headerValueParams = header.value.components(separatedBy: ";").map { $0.trimmingCharacters(in: .whitespaces) }
return headerValueParams.reduce(combined, { (results, token) -> [String] in
let parameterTokens = token.components(separatedBy: "=")
if parameterTokens.first == parameter, let value = parameterTokens.last {
return results + [value]
}
return results
})
}.first
}
}
public func parseMultiPartFormData() {
//dump()
//print("read multipart")
guard let contentTypeHeader = headers["content-type"] else {
return
}
let contentTypeHeaderTokens = contentTypeHeader.components(separatedBy: ";").map { $0.trimmingCharacters(in: .whitespaces) }
guard let contentType = contentTypeHeaderTokens.first, contentType == "multipart/form-data" else {
return
}
var boundary: String?
contentTypeHeaderTokens.forEach({
let tokens = $0.components(separatedBy: "=")
if let key = tokens.first, key == "boundary" && tokens.count == 2 {
boundary = tokens.last
}
})
//print("boundary = '\(boundary ?? "")'")
if let boundary = boundary, boundary.utf8.count > 0 {
parseMultiPartFormData(bytes, boundary: "--\(boundary)")
}
}
private func parseMultiPartFormData(_ data: [UInt8], boundary: String) {
var generator = data.makeIterator()
var isFirst = true
//print("reading parts")
while let part = nextMultiPart(&generator, boundary: boundary, isFirst: isFirst) {
if let name = part.name, !name.isEmpty{
//print("part has name '\(name)'")
if let fileName = part.fileName{
//print("part is file \(fileName)")
files[name] = MemoryFile(name: fileName, data: Data(part.body))
}
else {
//print("part is string")
if let value = String(bytes: part.body, encoding: .utf8) {
if var array = params[name] as? Array<String> {
array.append(value)
params[name] = array
} else {
var array = Array<String>()
array.append(value)
params[name] = array
}
}
}
}
isFirst = false
}
}
private func nextMultiPart(_ generator: inout IndexingIterator<[UInt8]>, boundary: String, isFirst: Bool) -> MultiPart? {
if isFirst {
guard nextUTF8MultiPartLine(&generator) == boundary else {
return nil
}
} else {
let _ = nextUTF8MultiPartLine(&generator)
}
var headers = [String: String]()
while let line = nextUTF8MultiPartLine(&generator), !line.isEmpty {
let tokens = line.components(separatedBy: ":")
if let name = tokens.first, let value = tokens.last, tokens.count == 2 {
headers[name.lowercased()] = value.trimmingCharacters(in: .whitespaces)
}
}
guard let body = nextMultiPartBody(&generator, boundary: boundary) else {
return nil
}
return MultiPart(headers: headers, body: body)
}
private static let CR = UInt8(13)
private static let NL = UInt8(10)
private func nextUTF8MultiPartLine(_ generator: inout IndexingIterator<[UInt8]>) -> String? {
var temp = [UInt8]()
while let value = generator.next() {
if value > Request.CR {
temp.append(value)
}
if value == Request.NL {
break
}
}
return String(bytes: temp, encoding: .utf8)
}
private func nextMultiPartBody(_ generator: inout IndexingIterator<[UInt8]>, boundary: String) -> [UInt8]? {
var body = [UInt8]()
let boundaryArray = [UInt8](boundary.utf8)
var matchOffset = 0
while let x = generator.next() {
matchOffset = ( x == boundaryArray[matchOffset] ? matchOffset + 1 : 0 )
body.append(x)
if matchOffset == boundaryArray.count {
body.removeSubrange(body.count-matchOffset ..< body.count)
if body.last == Request.NL {
body.removeLast()
if body.last == Request.CR {
body.removeLast()
}
}
return body
}
}
return nil
}
}

View File

@ -0,0 +1,311 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
import NIO
import NIOHTTP1
public class Request {
public var path: String = ""
public var method: HTTPMethod = .GET
public var uri: String = ""
public var headers: [String: String] = [:]
public var language = "en"
public var bytes = [UInt8]()
public var params: [String: Any] = [:]
public var files = [String:MemoryFile]()
public var session : Session? = nil
public var viewType : ViewType = .show
public var message : Message? = nil
public var formError: FormError? = nil
public var pageVars = [String: String]()
public var address : String?{
get{
headers["host"]
}
}
public var referer : String?{
get{
headers["referer"]
}
}
public var userAgent : String?{
get{
headers["user-agent"]
}
}
public var accept : String?{
get{
headers["accept"]
}
}
public var acceptLanguage : String?{
get{
headers["accept-language"]
}
}
public var keepAlive : Bool{
get{
headers["connection"] == "keep-alive"
}
}
public var contentTypeTokens : [String]{
get{
if let contentTypeHeader = headers["content-type"]{
return contentTypeHeader.components(separatedBy: ";").map { $0.trimmingCharacters(in: .whitespaces) }
}
return []
}
}
public var contentType : String{
get{
let tokens = contentTypeTokens
return tokens.first ?? ""
}
}
public var contentLength : Int?{
get{
Int(headers["content-length"] ?? "")
}
}
public var sessionId: String{
get{
if let cookies = headers["cookie"]?.split(";"){
for cookie in cookies{
let cookieParts = cookie.split("=")
if cookieParts[0].trim().lowercased() == "sessionid"{
return cookieParts[1].trim()
}
}
}
return ""
}
}
public var user : UserData?{
get{
session?.user
}
}
public var isLoggedIn: Bool {
get {
user != nil
}
}
public var userId: Int {
get {
user == nil ? 0 : user!.id
}
}
public var hasMessage: Bool {
get {
message != nil
}
}
public var hasFormError: Bool {
get {
formError != nil && !formError!.isEmpty
}
}
init(){
}
public func setLanguage(){
if var lang = acceptLanguage{
lang = lang.lowercased()
if lang.count > 2{
lang = String(lang[lang.startIndex...lang.index(lang.startIndex, offsetBy: 1)])
}
language = lang
}
else {
language = "en"
}
}
public func setParam(_ name: String, _ value: Any){
params[name] = value
}
public func removeParam(_ name: String){
params[name] = nil
}
public func getParam(_ name: String) -> Any?{
params[name]
}
public func getParam<T>(_ name: String, type: T.Type) -> T?{
getParam(name) as? T
}
public func getString(_ name: String, def: String = "") -> String{
if let s = getParam(name) as? String {
return s
}
if let arr = getParam(name) as? Array<String> {
return arr.first ?? def
}
return def
}
public func getStringArray(_ name: String) -> Array<String>?{
getParam(name) as? Array<String>
}
public func getInt(_ name: String, def: Int = -1) -> Int{
let s = getString(name)
return Int(s) ?? def
}
public func getIntArray(_ name: String) -> Array<Int>?{
if let stringSet = getStringArray(name){
var array = Array<Int>()
for s in stringSet{
if let i = Int(s){
array.append(i)
}
}
return array
}
return nil
}
public func getBool(_ name: String) -> Bool{
let s = getString(name)
return Bool(s) ?? false
}
public func getFile(_ name: String) -> MemoryFile?{
print("files: \(files.keys)")
return files[name]
}
public func setSessionAttribute(_ name: String, value: Any){
session?.setAttribute(name, value: value)
}
public func removeSessionAttribute(_ name: String){
session?.removeAttribute(name)
}
public func getSessionAttributeNames() -> Set<String>{
session?.getAttributeNames() ?? Set<String>()
}
public func getSessionAttribute(_ name: String) -> Any?{
session?.getAttribute(name)
}
public func getSessionAttribute<T>(_ name: String, type: T.Type) -> T?{
getSessionAttribute(name) as? T
}
public func getSessionString(_ name: String, def : String = "") -> String{
getSessionAttribute(name) as? String ?? def
}
public func getSessionInt(_ name: String, def: Int = -1) -> Int{
if let i = getSessionAttribute(name) as? Int{
return i
}
if let s = getSessionAttribute(name) as? String{
return Int(s) ?? def
}
return def
}
public func getSessionBool(_ name: String) -> Bool{
if let b = getSessionAttribute(name) as? Bool{
return b
}
if let s = getSessionAttribute(name) as? String{
return Bool(s) ?? false
}
return false
}
public func addPageVar(_ name: String,_ value: String){
pageVars[name] = value
}
public func addConditionalPageVar(_ name: String,_ value: String, if condition: Bool){
if condition{
addPageVar(name, value)
}
}
public func setMessage(_ msg: String, type: MessageType) {
message = Message(type: type, text: msg)
}
public func getFormError(create: Bool) -> FormError {
if formError == nil && create {
formError = FormError()
}
return formError!
}
public func addFormError(_ s: String) {
getFormError(create: true).addFormError(s)
}
public func addFormField(_ field: String) {
getFormError(create: true).addFormField(field)
}
public func addIncompleteField(_ field: String) {
getFormError(create: true).addFormField(field)
getFormError(create: false).formIncomplete = true
}
public func hasFormErrorField(_ name: String) -> Bool {
if formError == nil {
return false
}
return formError!.hasFormErrorField(name: name)
}
public func dump() {
Log.info(">>start request")
Log.info("method = \(method.rawValue)")
Log.info("uri = \(uri)")
Log.info("path = \(path)")
Log.info("params = \(params)")
Log.info("pageVars = \(pageVars)")
Log.info("address = \(address ?? "")")
Log.info("referer = \(referer ?? "")")
Log.info("user agent = \(userAgent ?? "")")
Log.info("keepAlive = \(keepAlive)")
Log.info("contentType = \(contentType)")
Log.info("content length = \(contentLength ?? -1)")
Log.info("sessionId = \(sessionId)")
if session != nil {
Log.info("sessionAttributes = \(session!.attributes)")
}
Log.info("<<end request")
}
}

View File

@ -0,0 +1,85 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
import NIO
import NIOHTTP1
public class Response {
public var headers = HTTPHeaders()
public var sessionId : String? = nil
public var body : ByteBuffer? = nil
public var status : HTTPResponseStatus = .ok
private var channel : Channel!
public func process(channel: Channel){
self.channel = channel
writeHeader()
writeBody()
end()
}
init(code: HTTPResponseStatus){
status = code
}
init(html: String){
status = .ok
headers.replaceOrAdd(name: "Content-Type", value: "text/html")
body = ByteBuffer(string: html)
}
init(json: String){
status = .ok
headers.replaceOrAdd(name: "Content-Type", value: "application/json")
body = ByteBuffer(string: json)
}
init(data: Data, fileName: String, contentType: String, download: Bool = false){
status = .ok
headers.replaceOrAdd(name: "Content-Type", value: contentType)
let disposition = "\(download ? "attachment" : "inline"); filename=\"\(fileName.toSafeWebName())\""
headers.replaceOrAdd(name: "Content-Disposition", value: disposition)
body = ByteBuffer(bytes: data)
}
public func setHeader(name: String, value : String){
headers.replaceOrAdd(name: name, value: value)
}
private func writeHeader() {
if let sessionId = sessionId{
headers.replaceOrAdd(name: "Set-Cookie", value: "sessionId=\(sessionId);path=/")
}
let head = HTTPResponseHead(version: .init(major: 1, minor: 1), status: status, headers: headers)
let part = HTTPServerResponsePart.head(head)
_ = channel.writeAndFlush(part).recover(handleError)
}
private func writeBody() {
if body != nil {
let part = HTTPServerResponsePart.body(.byteBuffer(body!))
_ = self.channel.writeAndFlush(part).recover(handleError)
}
}
private func handleError(_ error: Error) {
Log.error(error: error)
end()
}
private func end() {
_ = channel.writeAndFlush(HTTPServerResponsePart.end(nil)).map {
self.channel.close()
}
}
}

View File

@ -0,0 +1,92 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public struct Router {
public static let controllerPrefix = "/ctrl/"
public static let ajaxPrefix = "/ajax/"
public static let layoutPrefix = "/layout/"
public static let filesPrefix = "/files/"
public static let shutdownPrefix = "/shutdown/"
public static let htmlSuffix = ".html"
public static var instance = Router()
public func route(_ request: Request) -> Response?{
let path = rewritePath(requestPath: request.path)
// *.html
if path.hasSuffix(Router.htmlSuffix) {
//Log.info("html path: \(path)")
if let content = ContentContainer.instance.getContent(url: path){
if let controller = ControllerFactory.getDataController(type: content.type){
return controller.processRequest(method: "show", id: content.id, request: request)
}
}
return Response(code: .notFound)
}
// controllers
if path.hasPrefix(Router.controllerPrefix) || path.hasPrefix(Router.ajaxPrefix) {
let pathSegments = path.split("/")
//Log.info("controller path: \(String(describing: pathSegments))")
if pathSegments.count > 2 {
let controllerName = pathSegments[1]
if let controllerType = ControllerType.init(rawValue: controllerName) {
if let controller = ControllerFactory.getController(type: controllerType) {
let method = pathSegments[2]
var id: Int? = nil
if pathSegments.count > 3 {
id = Int(pathSegments[3])
}
return controller.processRequest(method: method, id: id, request: request)
}
}
}
return Response(code: .notFound)
}
// scontent files from files directory
if path.hasPrefix(Router.filesPrefix) {
return FileController.instance.show(request: request)
}
// static layout files from layout directory
if path.hasPrefix(Router.layoutPrefix) {
return StaticFileController.instance.processLayoutPath(path: path, request: request)
}
// shutdown request
if path.hasPrefix(Router.shutdownPrefix) {
let pathSegments = path.split("/")
if pathSegments.count > 1 {
let shutdownCode = pathSegments[1]
if shutdownCode == Statics.instance.shutdownCode {
DispatchQueue.global(qos: .userInitiated).async {
Application.instance.stop()
}
return Response(code: .ok)
}
else{
Log.warn("shutdown codes don't match")
}
}
return Response(code: .badRequest)
}
// static files from [resources]/web/
return StaticFileController.instance.processPath(path: path, request: request)
}
public func rewritePath(requestPath: String) -> String{
switch requestPath{
case "": fallthrough
case "/": return "/home.html"
default:
return requestPath
}
}
}

View File

@ -0,0 +1,48 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class Session {
public var sessionId = ""
public var timestamp = Date()
public var user: UserData? = nil
public var attributes = Dictionary<String, Any>()
init() {
timestamp = Date()
sessionId = generateID()
}
private static let asciiChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
private func generateID() -> String {
String((0..<32).map { _ in
Session.asciiChars.randomElement()!
})
}
public func getAttribute(_ name: String) -> Any? {
return attributes[name]
}
public func getAttributeNames() -> Set<String> {
Set(attributes.keys)
}
public func setAttribute(_ name: String, value: Any) {
attributes[name] = value
}
public func removeAttribute(_ name: String) {
attributes[name] = nil
}
}

View File

@ -0,0 +1,42 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public struct SessionCache{
public static var sessions = Dictionary<String, Session>()
private static let semaphore = DispatchSemaphore(value: 1)
public static func getSession(sessionId: String) -> Session{
lock()
defer{unlock()}
var session = sessions[sessionId]
if session != nil{
//Log.info("found session \(session!.sessionId)")
session!.timestamp = Date()
}
else{
session = Session()
Log.info("created new session with id \(session!.sessionId)")
sessions[session!.sessionId] = session
}
return session!
}
private static func lock(){
semaphore.wait()
}
private static func unlock(){
semaphore.signal()
}
}

View File

@ -0,0 +1,51 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class StaticFileController: Controller {
public static var instance = StaticFileController()
public func processPath(path: String, request: Request) -> Response?{
//Log.info("loading static file \(path)")
let fullPath = Paths.webDirectory.appendPath(path.makeRelativePath())
if let data : Data = Files.readFile(path: fullPath){
let contentType = MimeType.from(fullPath)
return Response(data: data, fileName: fullPath.lastPathComponent(), contentType: contentType)
}
Log.info("reading file from \(fullPath) failed")
return Response(code: .notFound)
}
public func processLayoutPath(path: String, request: Request) -> Response?{
//Log.info("loading static file \(path)")
var path = path
path.removeFirst(Router.layoutPrefix.count)
let fullPath = Paths.layoutDirectory.appendPath(path)
if let data : Data = Files.readFile(path: fullPath){
let contentType = MimeType.from(fullPath)
return Response(data: data, fileName: fullPath.lastPathComponent(), contentType: contentType)
}
Log.info("reading file from \(fullPath) failed")
return Response(code: .notFound)
}
public func ensureLayout(){
if Files.directoryIsEmpty(path: Paths.layoutDirectory) {
for sourcePath in Files.listAllFiles(dirPath: Paths.defaultLayoutDirectory){
let targetPath = Paths.layoutDirectory.appendPath(sourcePath.lastPathComponent())
if !Files.copyFile(from: sourcePath, to: targetPath){
Log.error("could not copy layout file \(sourcePath)")
}
}
}
}
}

View File

@ -0,0 +1,54 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class BreadcrumbTag: ServerPageTag {
override public class var type: TagType {
.spgBreadcrumb
}
override public func getHtml(request: Request) -> String {
var html = ""
let content = request.getSafeContent()
let parentIds = ContentContainer.instance.collectParentIds(contentId: content.id)
html.append("""
<section public class="col-12">
<ol public class="breadcrumb">
""")
for i in (0..<parentIds.count).reversed() {
if let content = ContentContainer.instance.getContent(id: parentIds[i]) {
html.append("""
<li public class="breadcrumb-item">
<a href="{{url}}">{{displayName}}
</a>
</li>
""".format(language: request.language, [
"url": content.getUrl().toUri(),
"displayName": content.displayName.toHtml()]
))
}
}
html.append("""
<li public class="breadcrumb-item">
<a>{{displayName}}
</a>
</li>
""".format(language: request.language, [
"displayName": content.displayName.toHtml()]
))
html.append("""
</ol>
</section>
""")
return html
}
}

View File

@ -0,0 +1,120 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class CkTreeTag: ServerPageTag {
override public class var type: TagType {
.spgCkTree
}
public var type = "image"
public var callBackNum = -1
override public func getHtml(request: Request) -> String {
var html = ""
type = getStringAttribute("type", request, def: "image")
callBackNum = request.getInt("CKEditorpublic funcNum", def: -1)
html.append("""
<section public class="treeSection">
<ul public class="tree pagetree">
""".format(language: request.language, nil))
if Right.hasUserReadRight(user: request.user, contentId: ContentData.ID_ROOT) {
html.append("""
<li public class="open">
<span>{{displayName}}</span>
""".format(language: request.language, ["displayName" : ContentContainer.instance.contentRoot.displayName]))
if SystemZone.hasUserSystemRight(user: request.user, zone: .contentEdit) {
html.append(getHtml(content: ContentContainer.instance.contentRoot, request: request))
}
html.append("""
</li>
""")
}
html.append("""
</ul>
</section>
""")
return html
}
private func getHtml(content: ContentData, request: Request) -> String {
var html = ""
html.append("""
<ul>
""")
html.append("""
<li public class="files open">
<span>{{_files}}</span>
""".format(language: request.language, nil))
// file icons
if Right.hasUserReadRight(user: request.user, content: content) {
html.append("""
<ul>
""")
for file in content.files {
html.append("""
<li>
<div public class="treeline">
<span public class="treeImage" id="{{id}}">
{{displayName}}
""".format(language: request.language, [
"id": String(file.id),
"displayName": file.displayName.toHtml(),
]))
if file.isImage {
html.append("""
<span public class="hoverImage">
<img src="{{previewUrl}}" alt="{{fileName)}}"/>
</span>
""".format(language: request.language, [
"fileName": file.fileName.toHtml(),
"previewUrl": file.previewUrl]))
}
// single file icons
html.append("""
</span>
<div public class="icons">
<a public class="icon fa fa-check-square-o" href="" onclick="return ckCallback('{{url}}')" title="{{_select}}"> </a>
</div>
</div>
</li>
""".format(language: request.language, [
"url": file.url.toUri()]))
}
html.append("""
</ul>
""")
}
html.append("""
</li>
""")
// child content
if !content.children.isEmpty {
for childData in content.children {
html.append("""
<li public class="open">
<span>{{displayName}}</span>
""".format(language: request.language, ["displayName" : childData.displayName]))
if Right.hasUserReadRight(user: request.user, content: childData) {
html.append(getHtml(content: childData, request: request))
}
html.append("""
</li>
""")
}
}
html.append("""
</ul>
""")
return html
}
}

View File

@ -0,0 +1,35 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class ContentTag: ServerPageTag{
public static var pageIncludeParam = "pageInclude"
override public class var type : TagType{
.spgContent
}
override public func getHtml(request: Request) -> String {
if let content = request.getContent(){
return content.displayContent(request: request)
}
else {
// page without content, e.g. user profile
if let pageInclude : String = request.getParam(ContentTag.pageIncludeParam) as? String{
if let html = ServerPageController.processPage(path: pageInclude, request: request) {
return html
}
}
}
return ""
}
}

View File

@ -0,0 +1,209 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class ContentTreeTag: ServerPageTag {
override public class var type: TagType {
.spgContentTree
}
override public func getHtml(request: Request) -> String {
var html = ""
html.append("""
<section public class="treeSection">
<div><a href="/ctrl/admin/clearClipboard">{{_clearClipboard}}</a></div>
<ul public class="tree pagetree">
""".format(language: request.language, nil))
if Right.hasUserReadRight(user: request.user, contentId: ContentData.ID_ROOT) {
html.append("""
<li public class="open">
<span>{{displayName}}</span>
""".format(language: request.language, ["displayName" : ContentContainer.instance.contentRoot.displayName]))
if SystemZone.hasUserSystemRight(user: request.user, zone: .contentEdit) {
html.append(getHtml(content: ContentContainer.instance.contentRoot, request: request))
}
html.append("""
</li>
""")
}
html.append("""
</ul>
</section>
<script type="text/javascript">
let $current = $('.current','.pagetree');
if ($current){
let $parents=$current.parents('li');
$parents.addpublic class("open");
}
</script>
""")
return html
}
private func getHtml(content: ContentData, request: Request) -> String {
var html = ""
let params = [
"type": content.type.rawValue,
"id": String(content.id),
"version": String(content.version),
"displayName": content.displayName.toHtml()]
if Right.hasUserEditRight(user: request.user, content: content) {
// icons
// show, edit, rights
html.append(
"""
<div public class="icons">
<a public class="icon fa fa-eye" href="" onclick="return linkTo('/ctrl/{{type}}/show/{{id}}');" title="{{_view}}"> </a>
<a public class="icon fa fa-pencil" href="" onclick="return openModalDialog('/ajax/{{type}}/openEditContentData/{{id}}');" title="{{_edit}}"> </a>
<a public class="icon fa fa-key" href="" onclick="return openModalDialog('/ajax/{{type}}/openEditRights/{{id}}');" title="{{_rights}}"> </a>
""".format(language: request.language, params))
// cut, copy
if content.id != ContentData.ID_ROOT {
html.append("""
<a public class ="icon fa fa-scissors" href = "" onclick = "return linkTo('/ctrl/{{type}}/cutContent/{{id}}');" title = "{{_cut}}"> </a>
<a public class ="icon fa fa-copy" href = "" onclick = "return linkTo('/ctrl/{{type}}/copyContent/{{id}}');" title = "{{_copy}}"> </a>
""".format(language: request.language, params))
}
// sort children
if !content.children.isEmpty {
html.append("""
<a public class ="icon fa fa-sort" href = "" onclick = "return openModalDialog('/ajax/{{type}}/openSortChildPages/{{id}}');" title = "{{_sortChildPages}}"> </a>
""".format(language: request.language, params))
}
// delete
if content.id != ContentData.ID_ROOT {
html.append("""
<a public class ="icon fa fa-trash-o" href = "" onclick = "if (confirmDelete()) return linkTo('/ctrl/{{type}}/deleteContent/{{id}}');" title = "{{_delete}}"> </a>
""".format(language: request.language, params))
}
// paste
if Clipboard.instance.hasData(type: .content) {
html.append("""
<a public class ="icon fa fa-paste" href = "/ctrl/{{type}}/pasteContent?parentId={{id}}&parentVersion={{version}}" title = "{{_pasteContent}}"> </a>
""".format(language: request.language, params))
}
// new content
if !content.childTypes.isEmpty {
html.append("""
<a public class ="icon fa fa-plus dropdown-toggle" data-toggle = "dropdown" title = "{{_newContent}}" > </a>
<div public class ="dropdown-menu">
""".format(language: request.language, nil))
for type in content.childTypes {
html.append("""
<a public class ="dropdown-item" onclick = "return openModalDialog('/ajax/{{type}}/openCreateContentData?parentId={{id}}&type={{pageType}}');">{{pageTypeName}}</a>
""".format(language: request.language, [
"type": content.type.rawValue,
"id": String(content.id),
"pageType": type.rawValue,
"pageTypeName": "_type.\(type.rawValue)".toLocalizedHtml(language: request.language)]))
}
html.append("""
</div>
""")
}
html.append("""
</div>
""")
}
// files
html.append("""
<ul>
""")
html.append("""
<li public class="files open">
<span>{{_files}}</span>
""".format(language: request.language, nil))
// file icons
if Right.hasUserEditRight(user: request.user, content: content) {
html.append("""
<div public class="icons">
""")
// paste
if Clipboard.instance.hasData(type: .file) {
html.append("""
<a public class="icon fa fa-paste" href="/ctrl/file/pasteFile?parentId={{id}}&parentVersion={{version}}" title="{{_pasteFile}}"> </a>
""".format(language: request.language, params))
}
// new file
html.append("""
<a public class="icon fa fa-plus" onclick="return openModalDialog('/ajax/file/openCreateFile?parentId={{id}}');" title="{{_newFile}}">
</a>
</div>
""".format(language: request.language, params))
}
if Right.hasUserEditRight(user: request.user, content: content) {
html.append("""
<ul>
""")
for file in content.files {
let fileParams = [
"id": String(file.id),
"displayName": file.displayName.toHtml(),
"fileName": file.fileName.toHtml(),
"url": file.url,
"previewUrl": file.previewUrl]
html.append("""
<li>
<div public class="treeline">
<span public class="treeImage" id="{{id}}">
{{displayName}}
""".format(language: request.language, fileParams))
if file.isImage {
html.append("""
<span public class="hoverImage">
<img src="{{previewUrl}}" alt="{{fileName)}}"/>
</span>
""".format(language: request.language, fileParams))
}
// single file icons
html.append("""
</span>
<div public class="icons">
<a public class="icon fa fa-eye" href="{{url}}" target="_blank" title="{{_view}}"> </a>
<a public class="icon fa fa-download" href="{{url}}?download=true" title="{{_download}}"> </a>
<a public class="icon fa fa-pencil" href="" onclick="return openModalDialog('/ajax/file/openEditFile/{{id}}');" title="{{_edit}}"> </a>
<a public class="icon fa fa-scissors" href="" onclick="return linkTo('/ctrl/file/cutFile/{{id}}');" title="{{_cut}}"> </a>
<a public class="icon fa fa-copy" href="" onclick="return linkTo('/ctrl/file/copyFile/{{id}}');" title="{{_copy}}"> </a>
<a public class="icon fa fa-trash-o" href="" onclick="if (confirmDelete()) return linkTo('/ctrl/file/deleteFile/{{id}}');" title="{{_delete}}"> </a>
</div>
</div>
</li>
""".format(language: request.language, fileParams))
}
html.append("""
</ul>
""")
}
html.append("""
</li>
""")
// child content
if !content.children.isEmpty {
for childData in content.children {
html.append("""
<li public class="open">
<span>{{displayName}}</span>
""".format(language: request.language, ["displayName" : childData.displayName]))
if Right.hasUserReadRight(user: request.user, content: childData) {
html.append(getHtml(content: childData, request: request))
}
html.append("""
</li>
""")
}
}
html.append("""
</ul>
""")
return html
}
}

View File

@ -0,0 +1,48 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FooterTag: ServerPageTag {
override public class var type: TagType {
.spgFooter
}
override public func getHtml(request: Request) -> String {
var html = ""
html.append("""
<ul public class="nav">
<li public class="nav-item">
<a public class="nav-link">&copy; {{copyRight}}
</a>
</li>
""".format(language: request.language, [
"copyRight": "_copyright".toLocalizedHtml(language: request.language)]
))
for child in ContentContainer.instance.contentRoot.children {
if child.navType == ContentData.NAV_TYPE_FOOTER && Right.hasUserReadRight(user: request.user, content: child) {
html.append("""
<li public class="nav-item">
<a public class="nav-link" href="{{url}}">{{displayName}}
</a>
</li>
""".format(language: request.language, [
"url": child.getUrl().toHtml(),
"displayName": child.displayName.toHtml()]
))
}
}
html.append("""
</ul>
""")
return html
}
}

View File

@ -0,0 +1,76 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FormCheckTag: ServerPageTag {
override public class var type: TagType {
.spgFormCheck
}
public var name = ""
public var value = ""
public var checked = false
public static var checkPreHtml =
"""
<span>
<input type="checkbox" name="{{name}}" value="{{value}}" {{checked}}/>
<label public class="form-check-label">
"""
public func getPreHtml() -> String{
FormCheckTag.checkPreHtml
}
public static var postHtml =
"""
</label>
</span>
"""
override public func getHtml(request: Request) -> String {
name = getStringAttribute("name", request)
value = getStringAttribute("value", request)
checked = getBoolAttribute("checked", request)
var html = getStartHtml(request: request)
html.append(getChildHtml(request: request))
html.append(getEndHtml(request: request))
return html
}
override public func getStartHtml(request: Request) -> String {
var html = ""
html.append(getPreHtml().format(language: request.language, [
"name": name.toHtml(),
"value": value.toHtml(),
"checked": checked ? "checked" : ""
]))
return html
}
override public func getEndHtml(request: Request) -> String {
var html = ""
html.append(FormCheckTag.postHtml)
return html
}
public static func getCheckHtml(request: Request, name: String, value: String, label: String, checked: Bool) -> String{
var html = checkPreHtml.format(language: request.language, [
"name": name.toHtml(),
"value": value.toHtml(),
"checked": checked ? "checked" : ""])
html.append(label.toHtml())
html.append(FormCheckTag.postHtml)
html.append("<br/>")
return html
}
}

View File

@ -0,0 +1,34 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FormDateTag: FormLineTag {
override public class var type: TagType {
.spgFormDate
}
override public func getPreControlHtml(request: Request) -> String{
var html = ""
let value = getStringAttribute("value", request)
html.append("""
<div public class="input-group date">
<input type="text" id="{{name}}" name="{{name}}" public class="form-control datepicker" value="{{value}}" />
</div>
<script type="text/javascript">$('#{{name}}').datepicker({language: '{{language}}'});</script>
""".format(language: request.language, [
"name" : name,
"value" : value.toHtml(),
"language" : request.language]
))
return html
}
}

View File

@ -0,0 +1,45 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FormEditorTag : FormLineTag{
override public class var type: TagType {
.spgFormEditor
}
public var type = "text"
public var hint = ""
public var height = ""
override public func getPreControlHtml(request: Request) -> String{
type = getStringAttribute("type", request, def: "text")
hint = getStringAttribute("hint", request)
height = getStringAttribute("height", request)
return """
<textarea id="{{name}}" name="{{name}}" data-editor="{{type}}" data-gutter="1" {{height}}>
""".format(language: request.language, [
"name" : name,
"type" : type,
"height" : height.isEmpty ? "" : "style=\"height:\(height)\""]
)
}
override public func getPostControlHtml(request: Request) -> String{
"""
</textarea>
<small id="{{name}}Hint" public class="form-text text-muted">{{hint}}</small>
""".format(language: request.language, [
"name" : name,
"hint" : hint.hasPrefix("_") ? hint.toLocalizedHtml(language: request.language) : hint.toHtml()]
)
}
}

View File

@ -0,0 +1,28 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FormErrorTag: ServerPageTag {
override public class var type: TagType {
.spgFormError
}
override public func getHtml(request: Request) -> String {
var html = ""
if request.hasFormError {
html.append("<div public class=\"formError\">\n")
html.append(request.getFormError(create: false).getFormErrorString().toHtmlMultiline())
html.append("</div>")
}
return html
}
}

View File

@ -0,0 +1,27 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FormFileTag: FormLineTag {
override public class var type: TagType {
.spgFormFile
}
override public func getPreControlHtml(request: Request) -> String {
let multiple = getBoolAttribute("multiple", request)
return """
<input type="file" public class="form-control-file" id="{{name}}" name="{{name}}" {{multiple}}>
""".format(language: request.language, [
"name": name,
"multiple": multiple ? "multiple" : ""])
}
}

View File

@ -0,0 +1,82 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FormLineTag: ServerPageTag {
override public class var type: TagType {
.spgFormLine
}
public var name = ""
public var label = ""
public var required = false
public var padded = false
override public func getHtml(request: Request) -> String {
name = getStringAttribute("name", request)
label = getStringAttribute("label", request)
required = getBoolAttribute("public required", request)
padded = getBoolAttribute("padded", request)
var html = ""
html.append(getStartHtml(request: request));
html.append(getPreControlHtml(request: request))
html.append(getChildHtml(request: request))
html.append(getPostControlHtml(request: request))
html.append(getEndHtml(request: request))
return html
}
override public func getStartHtml(request: Request) -> String {
var html = ""
html.append("<div public class=\"form-group row")
if request.hasFormErrorField(name) {
html.append(" error")
}
html.append("\">\n")
if label.isEmpty {
html.append("<div public class=\"col-md-3\"></div>")
} else {
html.append("<label public class=\"col-md-3 col-form-label\"")
if !name.isEmpty {
html.append(" for=\"")
html.append(name.toHtml())
html.append("\"")
}
html.append(">")
html.append(label.hasPrefix("_") ? label.toLocalizedHtml(language: request.language) : label.toHtml())
if (required) {
html.append(" <sup>*</sup>")
}
html.append("</label>\n")
}
html.append("<div public class=\"col-md-9")
if padded {
html.append(" padded")
}
html.append("\">\n");
return html
}
override public func getEndHtml(request: Request) -> String {
var html = ""
html.append("</div></div>")
return html
}
public func getPreControlHtml(request: Request) -> String {
""
}
public func getPostControlHtml(request: Request) -> String {
""
}
}

View File

@ -0,0 +1,26 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FormPasswordTag : FormLineTag{
override public class var type: TagType {
.spgFormPassword
}
override public func getPreControlHtml(request: Request) -> String{
"""
<input type="password" id="{{name}}" name="{{name}}" public class="form-control" />
""".format(language: request.language, [
"name" : name]
)
}
}

View File

@ -0,0 +1,40 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FormRadioTag: FormCheckTag {
override public class var type: TagType {
.spgFormRadio
}
public static let radioPreHtml =
"""
<span>
<input type="radio" name="{{name}}" value="{{value}}" {{checked}}/>
<label public class="form-check-label">
"""
override public func getPreHtml() -> String {
FormRadioTag.radioPreHtml
}
public static func getRadioHtml(request: Request, name: String, value: String, label: String, checked: Bool) -> String{
var html = radioPreHtml.format(language: request.language, [
"name": name.toHtml(),
"value": value.toHtml(),
"checked": checked ? "checked" : ""])
html.append(label.toHtml())
html.append(FormCheckTag.postHtml)
html.append("<br/>")
return html
}
}

View File

@ -0,0 +1,52 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FormSelectTag : FormLineTag{
override public class var type: TagType {
.spgFormSelect
}
public var onChange = ""
public static let preControlHtml =
"""
<select id="{{name}}" name="{{name}}" public class="form-control" {{onchange}}>
"""
public static let postControlHtml =
"""
</select>
"""
override public func getPreControlHtml(request: Request) -> String{
onChange = getStringAttribute("onchange", request)
return FormSelectTag.preControlHtml.format(language: request.language, [
"name" : name,
"onchange" : onChange.isEmpty ? "" : "onchange=\"\(onChange)\""]
)
}
override public func getPostControlHtml(request: Request) -> String{
FormSelectTag.postControlHtml
}
public static func getOptionHtml(request: Request, value: String, isSelected: Bool, text: String) -> String{
"""
<option value="{{value}}" {{isSelected}}>{{text}}</option>
""".format(language: request.language, [
"value" : value,
"isSelected": isSelected ? "selected" : "",
"text": text
])
}
}

View File

@ -0,0 +1,58 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FormTag: ServerPageTag {
override public class var type: TagType {
.spgForm
}
override public func getHtml(request: Request) -> String {
var html = ""
let name = getStringAttribute("name", request)
let url = getStringAttribute("url", request)
let multi = getBoolAttribute("multi", request)
let ajax = getBoolAttribute("ajax", request)
let target = getStringAttribute("target", request, def: "#modalDialog")
html.append("""
<form action="{{url}}" method="post" id="{{name}}" name="{{name}}" accept-charset="UTF-8"{{multi}}>
""".format(language: request.language, [
"url": url,
"name": name,
"multi": multi ? " enctype=\"multipart/form-data\"" : ""]
))
html.append(getChildHtml(request: request))
html.append("""
</form>
""")
if ajax {
html.append("""
<script type="text/javascript">
$('#{{name}}').submit(public function (event) {
var $this = $(this);
event.preventDefault();
var params = $this.{{serialize}}();
{{post}}('{{url}}', params,'{{target}}');
});
</script>
""".format(language: request.language, [
"name": name,
"serialize": multi ? "serializeFiles" : "serialize",
"post": multi ? "postMultiByAjax" : "postByAjax",
"url": url,
"target": target]
))
}
return html
}
}

View File

@ -0,0 +1,39 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FormTextAreaTag: FormLineTag {
override public class var type: TagType {
.spgFormTextarea
}
public var height = ""
public var value = ""
override public func getPreControlHtml(request: Request) -> String {
height = getStringAttribute( "height", request)
value = getStringAttribute("value", request)
return """
<textarea id="{{name}}" name="{{name}}" public class="form-control" {{value}}>
""".format(language: request.language, [
"name": name,
"height": height.isEmpty ? "" : "style=\"height:\(height)\"",
"value": value]
)
}
override public func getPostControlHtml(request: Request) -> String {
"""
</textarea>
"""
}
}

View File

@ -0,0 +1,33 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class FormTextTag : FormLineTag{
override public class var type: TagType {
.spgFormText
}
public var value = ""
public var maxLength : Int = 0
override public func getPreControlHtml(request: Request) -> String{
value = getStringAttribute("value", request)
maxLength = getIntAttribute("maxLength", request, def: 0)
return """
<input type="text" id="{{name}}" name="{{name}}" public class="form-control" value="{{value}}" {{maxLength}} />
""".format(language: request.language, [
"name" : name,
"value" : value,
"maxLength" : maxLength > 0 ? "maxlength=\" \(maxLength)\"" : ""]
)
}
}

View File

@ -0,0 +1,39 @@
/*
SwiftyBandika CMS - A Swift based Content Management System with JSON Database
Copyright (C) 2021 Michael Roennau
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
public class GroupListTag: ServerPageTag {
override public class var type: TagType {
.spgGroupList
}
override public func getHtml(request: Request) -> String {
var html = ""
let groupId = request.getInt("groupId")
for group in UserContainer.instance.groups {
html.append("""
<li public class="{{groupOpen}}">
<span>{{groupName}}&nbsp;({{groupId}})</span>
<div public class="icons">
<a public class="icon fa fa-pencil" href="" onclick="return openModalDialog('/ajax/group/openEditGroup/{{groupId}}');" title="{{_edit}}"> </a>
<a public class="icon fa fa-trash-o" href="" onclick="if (confirmDelete()) return linkTo('/ctrl/group/deleteGroup/{{groupId}}?version={{groupVersion}}');" title="{{_delete}}"> </a>
</div>
</li>
""".format(language: request.language, [
"groupOpen": String(group.id == groupId),
"groupName": group.name.toHtml(),
"groupId": String(group.id),
"groupVersion": String(group.version)
]))
}
return html
}
}

Some files were not shown because too many files have changed in this diff Show More