parent
58299d5f68
commit
135e711c4a
|
@ -0,0 +1,6 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/.idea
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
|
@ -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
|
||||
}
|
|
@ -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"]),
|
||||
]
|
||||
)
|
|
@ -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.
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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(){
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 ""
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 = ""
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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:"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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(""")
|
||||
case "'": result.append("'")
|
||||
case "&": result.append("&")
|
||||
case "<": result.append("<")
|
||||
case ">": result.append(">")
|
||||
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(""")
|
||||
case "'": result.append("'")
|
||||
case "&": result.append("&")
|
||||
case "<": result.append("<")
|
||||
case ">": result.append(">")
|
||||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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{
|
||||
""
|
||||
}
|
||||
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
])
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
""
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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">×</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 ""
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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 ""
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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">© {{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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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()]
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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" : ""])
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
""
|
||||
}
|
||||
|
||||
}
|
|
@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
])
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
"""
|
||||
}
|
||||
|
||||
}
|
|
@ -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)\"" : ""]
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -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}} ({{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
Loading…
Reference in New Issue