├── .github └── workflows │ └── test.yml ├── Dockerfile ├── LICENSE ├── README.rst ├── conf ├── app.conf ├── cache.conf ├── database.conf ├── fcgi.conf ├── modules.conf ├── nginx-fcgi.conf ├── session.conf └── storage.conf ├── controllers ├── ajax.go ├── article.go ├── auth.go ├── base.go ├── custom_controller.go ├── index.go ├── panel.go ├── profile.go └── websocket.go ├── core ├── block │ ├── base.go │ └── html.go ├── defaults │ ├── blocks │ │ └── news.go │ ├── menu.go │ └── modules │ │ └── news.go ├── lib │ ├── cache │ │ └── cache.go │ └── db │ │ ├── data.go │ │ └── db.go └── template │ └── path.go ├── go.mod ├── go.sum ├── integration_tests ├── base.go ├── conf │ ├── app.conf │ ├── cache.conf │ ├── database.conf │ ├── fcgi.conf │ ├── modules.conf │ ├── nginx-fcgi.conf │ ├── session.conf │ └── storage.conf ├── guestEndpoints_test.go └── views ├── main.go ├── models ├── article.go ├── base.go ├── blocks.go ├── form.go ├── image.go ├── jsonresponses.go ├── like.go ├── menu.go ├── modules.go ├── template.go ├── user.go └── validators.go ├── routers └── router.go ├── run.sh ├── static ├── img │ ├── article_cms.png │ ├── avatar │ │ ├── female-medium.jpg │ │ ├── female-small.jpg │ │ ├── female-thumbnail.jpg │ │ ├── male-medium.jpg │ │ ├── male-small.jpg │ │ └── male-thumbnail.jpg │ ├── btttcc.png │ └── xmmr.jpeg ├── js │ ├── layout-desktop.js │ ├── layout-mobile.js │ ├── layout-tablet.js │ ├── layout-watch.js │ ├── layout.js │ └── main.js └── uploads │ └── test.txt ├── utils ├── base.go ├── image.go ├── passwd.go └── session.go └── views └── default ├── article-editor.html ├── article.html ├── blocks └── html_block.html ├── index.html ├── layout.html ├── login.html ├── partial ├── html_head_desktop.html ├── html_head_mobile.html ├── html_head_tablet.html └── html_head_watch.html ├── profile-view.html ├── register.html ├── styles └── default │ ├── img │ └── avatar │ │ ├── female-medium.jpg │ │ ├── female-small.jpg │ │ ├── female-thumbnail.jpg │ │ ├── male-medium.jpg │ │ ├── male-small.jpg │ │ └── male-thumbnail.jpg │ └── js │ ├── layout-desktop.js │ ├── layout-mobile.js │ ├── layout-tablet.js │ ├── layout-watch.js │ ├── layout.js │ └── main.js └── user-profile.html /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - develop 7 | paths: 8 | - "**/*.go" 9 | - "go.mod" 10 | - "go.sum" 11 | - ".github/workflows/test.yml" 12 | pull_request: 13 | types: [opened, synchronize, reopened] 14 | branches: 15 | - master 16 | - develop 17 | paths: 18 | - "**/*.go" 19 | - "go.mod" 20 | - "go.sum" 21 | - ".github/workflows/test.yml" 22 | 23 | jobs: 24 | test: 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | go-version: [1.19.4] 29 | runs-on: ubuntu-latest 30 | 31 | steps: 32 | - name: Set up Go 33 | uses: actions/setup-go@v3 34 | with: 35 | go-version: ${{ matrix.go-version }} 36 | 37 | - name: Checkout codebase 38 | uses: actions/checkout@v2 39 | with: 40 | fetch-depth: 0 41 | 42 | - name: Install dependencies 43 | run: go get . 44 | 45 | - name: Run tests on sqlite3 46 | env: 47 | GOPATH: /home/runner/go 48 | ORM_DRIVER: sqlite3 49 | ORM_SOURCE: /tmp/sqlite3/orm_test.db 50 | run: | 51 | mkdir -p /tmp/sqlite3 && touch /tmp/sqlite3/orm_test.db 52 | go test -coverprofile=coverage_sqlite3.txt -covermode=atomic ./... 53 | 54 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19.4-alpine3.17 2 | LABEL "email"="dionyself@gmail.com" 3 | 4 | RUN apk add --no-cache git gcc g++ 5 | RUN mkdir /app 6 | RUN cd /app && git clone https://github.com/dionyself/golang-cms && sleep 3 7 | RUN cd /app/golang-cms && git pull origin master --tags && git checkout $(git tag --sort=committerdate | tail -1) && sleep 3 8 | RUN cd /app/golang-cms && GOMAXPROCS=1 go get github.com/smartystreets/goconvey && sleep 3 9 | RUN cd /app/golang-cms && GOMAXPROCS=1 go install github.com/smartystreets/goconvey && sleep 3 10 | RUN cd /app/golang-cms && GOMAXPROCS=1 go get github.com/beego/bee/v2 && sleep 3 11 | RUN cd /app/golang-cms && GOMAXPROCS=1 go install github.com/beego/bee/v2 && sleep 3 12 | RUN cd /app/golang-cms && GOMAXPROCS=1 go mod tidy 13 | 14 | EXPOSE 8080 15 | 16 | # Start app 17 | CMD sh /app/golang-cms/run.sh 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Dionys Rosario 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of golang-cms nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | .. image:: https://github.com/dionyself/golang-cms/actions/workflows/test.yml/badge.svg 3 | 4 | ########## 5 | GOLANG CMS 6 | ########## 7 | 8 | 9 | Open source Content Management System based on the BeeGO framework. 10 | 11 | ******** 12 | Features 13 | ******** 14 | 15 | * Hierarchical categories 16 | * Extensive support for multilingual websites. #TODO 17 | * Use the content blocks (& placeholders) in your own Templates 18 | * Edit content directly in the frontend on your pages. #TODO 19 | * Navigation rendering and extending from your apps. 20 | * SEO friendly urls. 21 | * Mobile support. 22 | * Editable areas & ads support 23 | 24 | **** 25 | Demo 26 | **** 27 | 28 | You will need to run the demo locally (Docker engine is required). 29 | Run a golang-cms instance on port 8080: 30 | 31 | - docker run -p 8080:8080 dionyself/golang-cms:latest 32 | 33 | Browse 127.0.0.1:8080 to see GolangCMS running. 34 | Login details. user: test, password: test 35 | 36 | - To create new articles visit http://127.0.0.1:8080/article/0/edit 37 | - To view an article visit http://127.0.0.1:8080/article//show 38 | - ex. http://127.0.0.1:8080/article/2/show 39 | 40 | Note: You will be running a pre-alpha version in testmode. 41 | Only Linux based OS are supported, please report any bug you find. 42 | if you can't see the demo please contact me. 43 | 44 | ***************************************************** 45 | Setting a development environment and/or contributing 46 | ***************************************************** 47 | 48 | Download, develop, compile and contribute! (requires a golang IDE, git and GO v1.19.4 or later) 49 | 50 | - git clone https://github.com/dionyself/golang-cms.git 51 | - cd golang-cms 52 | - go get github.com/beego/bee/v2 53 | - go install github.com/beego/bee/v2 54 | - bee run 55 | 56 | To run unittests, integration tests and Selenium Automation Testing. 57 | 58 | - go test ./... 59 | - goconvey ./integration_tests 60 | - webdriver ./automated_tests 61 | 62 | .. |bitcoin| image:: https://raw.githubusercontent.com/dionyself/golang-cms/master/static/img/btttcc.png 63 | :height: 230px 64 | :width: 230 px 65 | :alt: Donate with Bitcoin 66 | 67 | .. |xmr| image:: https://raw.githubusercontent.com/dionyself/golang-cms/master/static/img/xmmr.jpeg 68 | :height: 250px 69 | :width: 250 px 70 | :alt: Donate with Monero 71 | 72 | .. |paypal| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif 73 | :height: 100px 74 | :width: 200 px 75 | :alt: Donate with Paypal 76 | :target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=L4H5TUWZTZERS 77 | 78 | +------------------------------+ 79 | | Donate to this project | 80 | +-----------+----------+-------+ 81 | | Bitcoin | Paypal | XMR | 82 | +-----------+----------+-------+ 83 | | |bitcoin| + |paypal| + |xmr| + 84 | +-----------+----------+-------+ 85 | -------------------------------------------------------------------------------- /conf/app.conf: -------------------------------------------------------------------------------- 1 | RunMode = "test" 2 | 3 | # -- Application Config -- 4 | appname = golang-cms 5 | SessionOn = true 6 | DefaultDevice = "desktop" 7 | DirectoryIndex = true 8 | CopyRequestBody = true 9 | 10 | [prod] 11 | HttpAddr = "0.0.0.0" 12 | HttpPort = 80 13 | SessionProvider = redis 14 | DatabaseProvider = postgres 15 | ReCreateDatabase = false 16 | DatabaseDebugMode = false 17 | DatabaseLogging = false 18 | 19 | [dev] 20 | HttpAddr = "127.0.0.1" 21 | HttpPort = 8080 22 | SessionProvider = redis 23 | DatabaseProvider = mysql 24 | ReCreateDatabase = true 25 | DatabaseDebugMode = true 26 | DatabaseLogging = true 27 | 28 | [test] 29 | HttpAddr = "0.0.0.0" 30 | HttpPort = 8080 31 | SessionProvider = memory 32 | DatabaseProvider = sqlite3 33 | ReCreateDatabase = true 34 | DatabaseDebugMode = true 35 | DatabaseLogging = true 36 | 37 | include "session.conf" 38 | include "database.conf" 39 | include "cache.conf" 40 | include "storage.conf" 41 | include "modules.conf" 42 | include "fcgi.conf" 43 | -------------------------------------------------------------------------------- /conf/cache.conf: -------------------------------------------------------------------------------- 1 | [cacheConfig-prod] 2 | enabled = true 3 | dualMode = true 4 | flushInterval = 60 5 | defaultExpiry = 120 6 | redisMasterServer = {"key":"golangCMS","conn":"127.0.0.1:6379","dbNum":"0", "password":""} 7 | redisSlaveServer = {"key":"golangCMS","conn":"127.0.0.1:6379","dbNum":"0", "password":""} 8 | redisDatabaseIndex = 0 9 | redisKey = "collectionName" 10 | 11 | [cacheConfig-dev] 12 | enabled = true 13 | dualMode = true 14 | flushInterval = 60 15 | defaultExpiry = 120 16 | redisMasterServer = {"key":"golangCMS","conn":"127.0.0.1:6379","dbNum":"0", "password":""} 17 | redisSlaveServer = {"key":"golangCMS","conn":"127.0.0.1:6379","dbNum":"0", "password":""} 18 | redisDatabaseIndex = 0 19 | redisKey = "collectionName" 20 | 21 | [cacheConfig-test] 22 | enabled = true 23 | dualMode = false 24 | flushInterval = 60 25 | defaultExpiry = 120 26 | redisMasterServer = {"key":"golangCMS","conn":"127.0.0.1:6379","dbNum":"0", "password":""} 27 | redisSlaveServer = {"key":"golangCMS","conn":"127.0.0.1:6379","dbNum":"0", "password":""} 28 | redisDatabaseIndex = 0 29 | redisKey = "collectionName" 30 | -------------------------------------------------------------------------------- /conf/database.conf: -------------------------------------------------------------------------------- 1 | [databaseConfig-prod] 2 | replicaded = true 3 | masterServer = "localhost" 4 | masterServerPort = 0 5 | slaveServer = "localhost" 6 | slaveServerPort = 0 7 | databaseName = golang_cms 8 | databaseUser = golang_cms 9 | userPassword = golang_cms 10 | sqliteFile = data.db 11 | insertDemoData = true 12 | 13 | [databaseConfig-dev] 14 | replicaded = false 15 | masterServer = "localhost" 16 | masterServerPort = 0 17 | slaveServer = "localhost" 18 | slaveServerPort = 0 19 | databaseName = golang_cms 20 | databaseUser = golang_cms 21 | userPassword = golang_cms 22 | sqliteFile = data.db 23 | insertDemoData = true 24 | 25 | [databaseConfig-test] 26 | replicaded = false 27 | masterServer = "localhost" 28 | masterServerPort = 0 29 | slaveServer = "localhost" 30 | slaveServerPort = 0 31 | databaseName = golang_cms 32 | databaseUser = golang_cms 33 | userPassword = golang_cms 34 | sqliteFile = data.db 35 | insertDemoData = true 36 | -------------------------------------------------------------------------------- /conf/fcgi.conf: -------------------------------------------------------------------------------- 1 | # -- FastCGI Config -- 2 | usefcgi = false 3 | #HttpAddr="/tmp/golang-cms.sock" 4 | #HttpPort=0 5 | -------------------------------------------------------------------------------- /conf/modules.conf: -------------------------------------------------------------------------------- 1 | [modulesConfig-prod] 2 | Activated = true 3 | UsesBlocks = true 4 | Hidden = true 5 | 6 | [modulesConfig-dev] 7 | Activated = true 8 | UsesBlocks = true 9 | Hidden = true 10 | 11 | [modulesConfig-test] 12 | Activated = true 13 | UsesBlocks = true 14 | Hidden = true 15 | -------------------------------------------------------------------------------- /conf/nginx-fcgi.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name golang-cms.com; 4 | #root $GOLANG_CMS_STATIC; 5 | 6 | location / { 7 | error_log /var/log/nginx/golang-cms.error.log; 8 | access_log /var/log/nginx/golang-cms.log; 9 | include fastcgi_params; 10 | fastcgi_pass unix:/tmp/golang-cms.sock; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /conf/session.conf: -------------------------------------------------------------------------------- 1 | [sessionConfig-prod] 2 | memcacheServer = "127.0.0.1:11211" 3 | redisServer = "127.0.0.1:6379,100,password" 4 | memoryConfig = "" 5 | cookieName = "gosessionid" 6 | enableSetCookie = true 7 | omitempty = true 8 | gclifetime = 3600 9 | maxLifetime = 3600 10 | secure = false 11 | sessionIDHashFunc = sha1 12 | 13 | [sessionConfig-dev] 14 | redisServer = "127.0.0.1:6379,100" 15 | memoryConfig = "" 16 | cookieName = "gosessionid" 17 | enableSetCookie = true 18 | gclifetime = 3600 19 | maxLifetime = 3600 20 | secure = false 21 | sessionIDHashFunc = sha1 22 | 23 | [sessionConfig-test] 24 | memcacheServer = "127.0.0.1:11211" 25 | redisServer = "127.0.0.1:6379,100" 26 | memoryConfig = "" 27 | cookieName = "gosessionid" 28 | enableSetCookie = true 29 | omitempty = true 30 | gclifetime = 3600 31 | maxLifetime = 3600 32 | secure = false 33 | sessionIDHashFunc = sha1 34 | -------------------------------------------------------------------------------- /conf/storage.conf: -------------------------------------------------------------------------------- 1 | [localStorageConfig-prod] 2 | enabled = true 3 | mode = "custom" 4 | sshUser = "test" 5 | servers = ["127.0.0.1", "127.0.0.2", "127.0.0.2"] 6 | syncOrigin = 127.0.0.1 7 | useZFSreplication = false 8 | 9 | [localStorageConfig-dev] 10 | enabled = true 11 | mode = "diffuse" 12 | sshUser = "test" 13 | syncTime = 7200 14 | servers = ["127.0.0.1", "127.0.0.2", "127.0.0.2"] 15 | syncOrigin = 127.0.0.1 16 | useZFSreplication = false 17 | 18 | [localStorageConfig-test] 19 | enabled = true 20 | backupEnabled = True 21 | mode = "single" 22 | syncTime = 10 23 | storageUser = "test" 24 | customTarget = "127.0.0.1" 25 | syncTargets = "127.0.0.1 127.0.0.2 127.0.0.2" 26 | syncOrigin = 127.0.0.1 27 | targetFolder = ./static/uploads 28 | originFolder = ./static/uploads 29 | syncBackup = 127.0.0.1 30 | backupFolder = /tmp 31 | useZFSreplication = false 32 | -------------------------------------------------------------------------------- /controllers/ajax.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "image" 7 | "io" 8 | "mime/multipart" 9 | "strconv" 10 | 11 | "github.com/dionyself/golang-cms/models" 12 | "github.com/dionyself/golang-cms/utils" 13 | ) 14 | 15 | type AjaxController struct { 16 | BaseController 17 | } 18 | 19 | // generic Ajax 20 | func (CTRL *AjaxController) PostImage() { 21 | form := new(models.ImageForm) 22 | if err := CTRL.ParseForm(form); err != nil { 23 | CTRL.Abort("401") 24 | } else { 25 | if form.Validate() { 26 | if rawFile, fileHeader, err := CTRL.GetFile("File"); err == nil && utils.Contains(utils.SuportedMimeTypes["images"], fileHeader.Header["Conten-Type"][0]) { 27 | defer rawFile.Close() 28 | newSession := utils.GetRandomString(16) 29 | go CTRL.uploadAndRegisterIMG(newSession, rawFile, fileHeader, form) 30 | response := map[string]string{"status": "started", "sessionId": newSession} 31 | CTRL.Data["json"] = &response 32 | } 33 | } 34 | } 35 | CTRL.ServeJSON() 36 | } 37 | 38 | func (CTRL *AjaxController) uploadAndRegisterIMG(sessionKey string, img io.Reader, fileHeader *multipart.FileHeader, form *models.ImageForm) { 39 | var croppedImg image.Image 40 | var targets map[string][2]int 41 | status := map[string]string{} 42 | CTRL.cache.Set(sessionKey, `{"status":"starting..."}`, 30) 43 | json.Unmarshal([]byte(form.Targets), &targets) 44 | for target, coords := range targets { 45 | if !utils.ContainsKey(utils.ImageSizes, target) { 46 | continue 47 | } 48 | croppedImg, _ = utils.CropImage(img, fileHeader.Header["Conten-Type"][0], target, coords) 49 | if croppedImg != nil { 50 | utils.UploadImage(target, croppedImg) 51 | } 52 | status[target] = "done" 53 | CTRL.cache.Set(sessionKey, status, 30) 54 | } 55 | user := CTRL.Data["user"].(models.User) 56 | newImg := new(models.Image) 57 | newImg.User = &user 58 | CTRL.db.Insert(newImg) 59 | CTRL.db.Insert(user) 60 | status["image_id"] = fmt.Sprint(user.Id) 61 | CTRL.cache.Set(sessionKey, status, 30) 62 | } 63 | 64 | func (CTRL *AjaxController) GetImageUploadStatus() { 65 | imgID, err := strconv.Atoi(CTRL.Ctx.Input.Param(":id")) 66 | if err != nil { 67 | CTRL.Abort("403") 68 | } 69 | data := map[string]string{} 70 | if err := json.Unmarshal(CTRL.Ctx.Input.RequestBody, &data); err != nil { 71 | CTRL.Ctx.Output.SetStatus(400) 72 | } 73 | if status, err := CTRL.cache.GetMap(data["sessionKey"], 30); !err { 74 | data = status 75 | data["image_id"] = fmt.Sprint(imgID) 76 | } 77 | CTRL.Data["json"] = &data 78 | CTRL.ServeJSON() 79 | } 80 | -------------------------------------------------------------------------------- /controllers/article.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/dionyself/golang-cms/models" 8 | ) 9 | 10 | // ArticleController ... 11 | type ArticleController struct { 12 | BaseController 13 | } 14 | 15 | // Get Displays Article by id 16 | func (CTRL *ArticleController) Get() { 17 | ArtID, err := strconv.Atoi(CTRL.Ctx.Input.Param(":id")) 18 | if err != nil { 19 | CTRL.Abort("403") 20 | } 21 | db := CTRL.GetDB("default") 22 | if ArtID == 0 { 23 | CTRL.Data["form"] = models.ArticleForm{} 24 | cats := new([]*models.Category) 25 | db.QueryTable("category").All(cats) 26 | CTRL.Data["Categories"] = *cats 27 | CTRL.ConfigPage("article-editor.html") 28 | } else { 29 | Art := new(models.Article) 30 | Art.Id = ArtID 31 | db.Read(Art, "Id") 32 | CTRL.Data["Article"] = Art 33 | CTRL.ConfigPage("article.html") 34 | } 35 | } 36 | 37 | // Post create/update article 38 | func (CTRL *ArticleController) Post() { 39 | form := new(models.ArticleForm) 40 | Art := new(models.Article) 41 | if err := CTRL.ParseForm(form); err != nil { 42 | CTRL.Abort("401") 43 | } else { 44 | db := CTRL.GetDB() 45 | if !form.Validate() { 46 | CTRL.Data["form"] = form 47 | cats := new([]*models.Category) 48 | db.QueryTable("category").All(cats) 49 | CTRL.Data["Categories"] = cats 50 | CTRL.ConfigPage("article-editor.html") 51 | for key, msg := range form.InvalidFields { 52 | fmt.Println(key, msg) 53 | } 54 | } else { 55 | cat := new(models.Category) 56 | cat.Id = form.Category 57 | db.Read(cat, "Id") 58 | Art.Category = cat 59 | user := CTRL.Data["user"].(models.User) 60 | Art.User = &user 61 | Art.Title = form.Title 62 | Art.Content = form.Content 63 | Art.AllowComments = form.AllowComments 64 | db.Insert(Art) 65 | CTRL.Data["Article"] = Art 66 | CTRL.ConfigPage("article.html") 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /controllers/auth.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dionyself/golang-cms/models" 7 | "github.com/dionyself/golang-cms/utils" 8 | 9 | "github.com/beego/beego/v2/client/orm" 10 | "github.com/beego/beego/v2/server/web" 11 | "github.com/beego/beego/v2/server/web/context" 12 | ) 13 | 14 | var sessionName, _ = web.AppConfig.String("SessionName") 15 | 16 | // LoginController ... 17 | type LoginController struct { 18 | BaseController 19 | } 20 | 21 | // LoginView ... 22 | func (CTRL *LoginController) LoginView() { 23 | CTRL.ConfigPage("login.html") 24 | } 25 | 26 | // Login authenticates the user 27 | func (CTRL *LoginController) Login() { 28 | username := CTRL.GetString("username") 29 | password := CTRL.GetString("password") 30 | backTo := CTRL.GetString("back_to") 31 | 32 | var user models.User 33 | if VerifyUser(&user, username, password) { 34 | CTRL.SetSession(sessionName, user.Id) 35 | if backTo != "" { 36 | CTRL.Redirect("/"+backTo, 302) 37 | } else { 38 | CTRL.Redirect("/profile/0/show", 302) 39 | } 40 | } else { 41 | CTRL.Redirect("/register", 302) 42 | } 43 | 44 | } 45 | 46 | // Logout ... 47 | func (CTRL *LoginController) Logout() { 48 | CTRL.DelSession(sessionName) 49 | CTRL.Redirect("/login", 302) 50 | } 51 | 52 | // RegisterView displays register form 53 | func (CTRL *LoginController) RegisterView() { 54 | CTRL.ConfigPage("register.html") 55 | } 56 | 57 | // Register the user 58 | func (CTRL *LoginController) Register() { 59 | form := new(models.RegisterForm) 60 | if err := CTRL.ParseForm(form); err != nil { 61 | CTRL.Abort("401") 62 | } 63 | 64 | if form.Validate() { 65 | salt := utils.GetRandomString(10) 66 | encodedPwd := salt + "$" + utils.EncodePassword(form.Password, salt) 67 | 68 | o := CTRL.GetDB() 69 | profile := new(models.Profile) 70 | profile.Age = 0 71 | profile.Avatar = "female" 72 | if form.Gender { 73 | profile.Avatar = "male" 74 | } 75 | profile.Gender = form.Gender 76 | user := new(models.User) 77 | user.Profile = profile 78 | user.Username = form.Username 79 | user.Password = encodedPwd 80 | user.Rands = salt 81 | fmt.Println(o.Insert(profile)) 82 | fmt.Println(o.Insert(user)) 83 | 84 | CTRL.Redirect("/", 302) 85 | 86 | } else { 87 | CTRL.Data["form"] = form 88 | for key, msg := range form.InvalidFields { 89 | fmt.Println(key, msg) 90 | } 91 | CTRL.ConfigPage("register.html") 92 | } 93 | } 94 | 95 | // HasUser checks if user exists in db 96 | func HasUser(user *models.User, username string) bool { 97 | var err error 98 | qs := orm.NewOrm() 99 | user.Username = username 100 | err = qs.Read(user, "Username") 101 | return err == nil 102 | } 103 | 104 | // VerifyPassword checks if pwd is correct 105 | func VerifyPassword(rawPwd, encodedPwd string) bool { 106 | var salt, encoded string 107 | salt = encodedPwd[:10] 108 | encoded = encodedPwd[11:] 109 | return utils.EncodePassword(rawPwd, salt) == encoded 110 | } 111 | 112 | // VerifyUser virifies user credentials 113 | func VerifyUser(user *models.User, username, password string) (success bool) { 114 | if !HasUser(user, username) { 115 | return 116 | } 117 | if VerifyPassword(password, user.Password) { 118 | success = true 119 | } 120 | return 121 | } 122 | 123 | // AuthRequest "filter" to limit request based on sessionid 124 | var AuthRequest = func(ctx *context.Context) { 125 | uid, ok := ctx.Input.Session(sessionName).(int) 126 | if !ok && ctx.Input.URI() != "/login" && ctx.Input.URI() != "/register" { 127 | ctx.Redirect(302, "/login") 128 | return 129 | } 130 | var user models.User 131 | var err error 132 | qs := orm.NewOrm() 133 | user.Id = uid 134 | err = qs.Read(&user, "Id") 135 | if err != nil { 136 | ctx.Redirect(302, "/login") 137 | return 138 | } 139 | qs.LoadRelated(&user, "Profile") 140 | ctx.Input.SetData("user", user) 141 | } 142 | -------------------------------------------------------------------------------- /controllers/base.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "strconv" 5 | 6 | mobiledetect "github.com/Shaked/gomobiledetect" 7 | "github.com/beego/beego/v2/client/orm" 8 | "github.com/beego/beego/v2/server/web" 9 | "github.com/beego/beego/v2/server/web/context" 10 | "github.com/dionyself/golang-cms/core/block" 11 | "github.com/dionyself/golang-cms/core/defaults" 12 | "github.com/dionyself/golang-cms/core/lib/cache" 13 | database "github.com/dionyself/golang-cms/core/lib/db" 14 | "github.com/dionyself/golang-cms/core/template" 15 | "github.com/dionyself/golang-cms/utils" 16 | ) 17 | 18 | // BaseController Extendable 19 | type BaseController struct { 20 | CustomController //web.Controller 21 | db orm.Ormer 22 | cache cache.CACHE 23 | } 24 | 25 | // ConfigPage receives template name and makes basic config to render it 26 | func (CTRL *BaseController) ConfigPage(page string) { 27 | CTRL.GetDB() 28 | CTRL.GetCache() 29 | theme := template.GetActiveTheme(false) 30 | CTRL.Layout = theme[0] + "/" + "layout.html" 31 | device := CTRL.Ctx.Input.GetData("device_type").(string) 32 | CTRL.LayoutSections = make(map[string]string) 33 | CTRL.LayoutSections["Head"] = theme[0] + "/" + "partial/html_head_" + device + ".html" 34 | CTRL.TplName = theme[0] + "/" + page 35 | CTRL.Data["Theme"] = theme[0] 36 | CTRL.Data["Style"] = theme[1] 37 | CTRL.Data["ModuleMenu"] = CTRL.GetModuleMenu() 38 | CTRL.GetBlocks() 39 | //CTRL.GetActiveModule() 40 | //CTRL.GetActiveCategory() 41 | //CTRL.GetActiveAds() 42 | } 43 | 44 | func (CTRL *BaseController) GetBlocks() map[string]string { 45 | // TODO : get blocks and set block content 46 | // loadedBlocks := CTRL.cache.GetMap(fmt.Sprintf("activeblocks/%s/%s", module, session_id) , expirationTime) 47 | loadedBlocks := make(map[string]string) 48 | ActiveBlocks := block.GetActiveBlocks(false) 49 | for _, CurentBlock := range ActiveBlocks { 50 | cblock := block.Blocks[CurentBlock] 51 | cblockSectionName := "Block_" + strconv.Itoa(cblock.GetPosition()) 52 | cblockSectionData := cblockSectionName + "_Data" 53 | loadedBlocks[cblockSectionName] = cblock.GetTemplatePath() 54 | CTRL.Data[cblockSectionData] = cblock.GetContent() 55 | } 56 | CTRL.LayoutSections = utils.MergeMaps(CTRL.LayoutSections, loadedBlocks) 57 | return CTRL.LayoutSections 58 | } 59 | 60 | func (CTRL *BaseController) GetActiveAds() map[string]string { 61 | // loadedAds := CTRL.cache.GetMap(fmt.Sprintf("activeAds/%s/%s", category, session_id) , expirationTime) 62 | return make(map[string]string) 63 | } 64 | 65 | // GetCache set the cache connector into our controller 66 | func (CTRL *BaseController) GetCache() { 67 | CTRL.cache = cache.MainCache 68 | } 69 | 70 | // GetDB set the orm connector into our controller 71 | // if repication activated we use slave to Slave 72 | func (CTRL *BaseController) GetDB(db ...string) orm.Ormer { 73 | if len(db) > 0 { 74 | CTRL.db = database.MainDatabase.GetOrm(db[0]) 75 | } else { 76 | CTRL.db = database.MainDatabase.GetOrm("") 77 | } 78 | return CTRL.db 79 | } 80 | 81 | // GetModuleMenu retrieves menu 82 | func (CTRL *BaseController) GetModuleMenu() string { 83 | output := defaults.GetDefaultMenu() 84 | return output 85 | } 86 | 87 | // GetContent gets contents 88 | func (CTRL *BaseController) GetContent() string { 89 | output := defaults.GetDefaultMenu() 90 | return output 91 | } 92 | 93 | // DetectUserAgent detects device type and set it into a cookie 94 | var DetectUserAgent = func(ctx *context.Context) { 95 | deviceDetector := mobiledetect.NewMobileDetect(ctx.Request, nil) 96 | ctx.Request.ParseForm() 97 | device := "" 98 | if len(ctx.Request.Form["device_type"]) != 0 { 99 | device = ctx.Request.Form["device_type"][0] 100 | } 101 | if device == "" { 102 | device = ctx.Input.Cookie("Device-Type") 103 | } 104 | if device == "" { 105 | if deviceDetector.IsMobile() { 106 | device = "mobile" 107 | } 108 | if deviceDetector.IsTablet() { 109 | device = "tablet" 110 | } 111 | if device == "" { 112 | device, _ = web.AppConfig.String("DefaultDevice") 113 | if device == "" { 114 | device = "desktop" 115 | } 116 | } 117 | } 118 | ctx.Output.Cookie("Device-Type", device) 119 | ctx.Input.SetData("device_type", device) 120 | } 121 | -------------------------------------------------------------------------------- /controllers/custom_controller.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package controllers 16 | 17 | import ( 18 | "bytes" 19 | context2 "context" 20 | "errors" 21 | "fmt" 22 | "html/template" 23 | "io" 24 | "mime/multipart" 25 | "net/http" 26 | "net/url" 27 | "os" 28 | "reflect" 29 | "strconv" 30 | "strings" 31 | "sync" 32 | 33 | "google.golang.org/protobuf/proto" 34 | 35 | "github.com/beego/beego/v2/server/web" 36 | 37 | "github.com/beego/beego/v2/server/web/context" 38 | "github.com/beego/beego/v2/server/web/context/param" 39 | "github.com/beego/beego/v2/server/web/session" 40 | ) 41 | 42 | var ( 43 | // ErrAbort custom error when user stop request handler manually. 44 | ErrAbort = errors.New("user stop run") 45 | // GlobalControllerRouter store comments with controller. pkgpath+controller:comments 46 | GlobalControllerRouter = make(map[string][]ControllerComments) 47 | copyBufferPool sync.Pool 48 | ) 49 | 50 | const ( 51 | bytePerKb = 1024 52 | copyBufferKb = 32 53 | filePerm = 0o666 54 | ) 55 | 56 | func init() { 57 | copyBufferPool.New = func() interface{} { 58 | return make([]byte, bytePerKb*copyBufferKb) 59 | } 60 | } 61 | 62 | // ControllerFilter store the filter for controller 63 | type ControllerFilter struct { 64 | Pattern string 65 | Pos int 66 | Filter web.FilterFunc 67 | ReturnOnOutput bool 68 | ResetParams bool 69 | } 70 | 71 | // ControllerFilterComments store the comment for controller level filter 72 | type ControllerFilterComments struct { 73 | Pattern string 74 | Pos int 75 | Filter string // NOQA 76 | ReturnOnOutput bool 77 | ResetParams bool 78 | } 79 | 80 | // ControllerImportComments store the import comment for controller needed 81 | type ControllerImportComments struct { 82 | ImportPath string 83 | ImportAlias string 84 | } 85 | 86 | // ControllerComments store the comment for the controller method 87 | type ControllerComments struct { 88 | Method string 89 | Router string 90 | Filters []*ControllerFilter 91 | ImportComments []*ControllerImportComments 92 | FilterComments []*ControllerFilterComments 93 | AllowHTTPMethods []string 94 | Params []map[string]string 95 | MethodParams []*param.MethodParam 96 | } 97 | 98 | // ControllerCommentsSlice implements the sort interface 99 | type ControllerCommentsSlice []ControllerComments 100 | 101 | func (p ControllerCommentsSlice) Len() int { return len(p) } 102 | 103 | func (p ControllerCommentsSlice) Less(i, j int) bool { return p[i].Router < p[j].Router } 104 | 105 | func (p ControllerCommentsSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 106 | 107 | // Controller defines some basic http request handler operations, such as 108 | // http context, template and view, session and xsrf. 109 | type CustomController struct { 110 | // context data 111 | Ctx *context.Context 112 | Data map[interface{}]interface{} 113 | 114 | // route controller info 115 | controllerName string 116 | actionName string 117 | methodMapping map[string]func() //method:routertree 118 | AppController interface{} 119 | 120 | // template data 121 | TplName string 122 | ViewPath string 123 | Layout string 124 | LayoutSections map[string]string // the key is the section name and the value is the template name 125 | TplPrefix string 126 | TplExt string 127 | EnableRender bool 128 | 129 | // xsrf data 130 | EnableXSRF bool 131 | _xsrfToken string 132 | XSRFExpire int 133 | 134 | // session 135 | CruSession session.Store 136 | } 137 | 138 | // ControllerInterface is an interface to uniform all controller handler. 139 | type ControllerInterface interface { 140 | Init(ct *context.Context, controllerName, actionName string, app interface{}) 141 | Prepare() 142 | Get() 143 | Post() 144 | Delete() 145 | Put() 146 | Head() 147 | Patch() 148 | Options() 149 | Trace() 150 | Finish() 151 | Render() error 152 | XSRFToken() string 153 | CheckXSRFCookie() bool 154 | HandlerFunc(fn string) bool 155 | URLMapping() 156 | } 157 | 158 | // Init generates default values of controller operations. 159 | func (c *CustomController) Init(ctx *context.Context, controllerName, actionName string, app interface{}) { 160 | c.Layout = "" 161 | c.TplName = "" 162 | c.controllerName = controllerName 163 | c.actionName = actionName 164 | c.Ctx = ctx 165 | c.TplExt = "tpl" 166 | c.AppController = app 167 | c.EnableRender = true 168 | c.EnableXSRF = true 169 | c.Data = ctx.Input.Data() 170 | c.methodMapping = make(map[string]func()) 171 | } 172 | 173 | // Prepare runs after Init before request function execution. 174 | func (c *CustomController) Prepare() {} 175 | 176 | // Finish runs after request function execution. 177 | func (c *CustomController) Finish() {} 178 | 179 | // Get adds a request function to handle GET request. 180 | func (c *CustomController) Get() { 181 | http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", http.StatusMethodNotAllowed) 182 | } 183 | 184 | // Post adds a request function to handle POST request. 185 | func (c *CustomController) Post() { 186 | http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", http.StatusMethodNotAllowed) 187 | } 188 | 189 | // Delete adds a request function to handle DELETE request. 190 | func (c *CustomController) Delete() { 191 | http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", http.StatusMethodNotAllowed) 192 | } 193 | 194 | // Put adds a request function to handle PUT request. 195 | func (c *CustomController) Put() { 196 | http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", http.StatusMethodNotAllowed) 197 | } 198 | 199 | // Head adds a request function to handle HEAD request. 200 | func (c *CustomController) Head() { 201 | http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", http.StatusMethodNotAllowed) 202 | } 203 | 204 | // Patch adds a request function to handle PATCH request. 205 | func (c *CustomController) Patch() { 206 | http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", http.StatusMethodNotAllowed) 207 | } 208 | 209 | // Options adds a request function to handle OPTIONS request. 210 | func (c *CustomController) Options() { 211 | http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", http.StatusMethodNotAllowed) 212 | } 213 | 214 | // Trace adds a request function to handle Trace request. 215 | // this method SHOULD NOT be overridden. 216 | // https://tools.ietf.org/html/rfc7231#section-4.3.8 217 | // The TRACE method requests a remote, application-level loop-back of 218 | // the request message. The final recipient of the request SHOULD 219 | // reflect the message received, excluding some fields described below, 220 | // back to the client as the message body of a 200 (OK) response with a 221 | // Content-Type of "message/http" (Section 8.3.1 of [RFC7230]). 222 | func (c *CustomController) Trace() { 223 | ts := func(h http.Header) (hs string) { 224 | for k, v := range h { 225 | hs += fmt.Sprintf("\r\n%s: %s", k, v) 226 | } 227 | return 228 | } 229 | hs := fmt.Sprintf("\r\nTRACE %s %s%s\r\n", c.Ctx.Request.RequestURI, c.Ctx.Request.Proto, ts(c.Ctx.Request.Header)) 230 | c.Ctx.Output.Header("Content-Type", "message/http") 231 | c.Ctx.Output.Header("Content-Length", fmt.Sprint(len(hs))) 232 | c.Ctx.Output.Header("Cache-Control", "no-cache, no-store, must-revalidate") 233 | c.Ctx.WriteString(hs) 234 | } 235 | 236 | // HandlerFunc call function with the name 237 | func (c *CustomController) HandlerFunc(fnname string) bool { 238 | if v, ok := c.methodMapping[fnname]; ok { 239 | v() 240 | return true 241 | } 242 | return false 243 | } 244 | 245 | // URLMapping register the internal Controller router. 246 | func (c *CustomController) URLMapping() {} 247 | 248 | // Bind if the content type is form, we read data from form 249 | // otherwise, read data from request body 250 | func (c *CustomController) Bind(obj interface{}) error { 251 | return c.Ctx.Bind(obj) 252 | } 253 | 254 | // BindYAML only read data from http request body 255 | func (c *CustomController) BindYAML(obj interface{}) error { 256 | return c.Ctx.BindYAML(obj) 257 | } 258 | 259 | // BindForm read data from form 260 | func (c *CustomController) BindForm(obj interface{}) error { 261 | return c.Ctx.BindForm(obj) 262 | } 263 | 264 | // BindJSON only read data from http request body 265 | func (c *CustomController) BindJSON(obj interface{}) error { 266 | return c.Ctx.BindJSON(obj) 267 | } 268 | 269 | // BindProtobuf only read data from http request body 270 | func (c *CustomController) BindProtobuf(obj proto.Message) error { 271 | return c.Ctx.BindProtobuf(obj) 272 | } 273 | 274 | // BindXML only read data from http request body 275 | func (c *CustomController) BindXML(obj interface{}) error { 276 | return c.Ctx.BindXML(obj) 277 | } 278 | 279 | // Mapping the method to function 280 | func (c *CustomController) Mapping(method string, fn func()) { 281 | c.methodMapping[method] = fn 282 | } 283 | 284 | // Render sends the response with rendered template bytes as text/html type. 285 | func (c *CustomController) Render() error { 286 | if !c.EnableRender { 287 | return nil 288 | } 289 | rb, err := c.RenderBytes() 290 | if err != nil { 291 | return err 292 | } 293 | 294 | if c.Ctx.ResponseWriter.Header().Get("Content-Type") == "" { 295 | c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8") 296 | } 297 | 298 | return c.Ctx.Output.Body(rb) 299 | } 300 | 301 | // RenderString returns the rendered template string. Do not send out response. 302 | func (c *CustomController) RenderString() (string, error) { 303 | b, e := c.RenderBytes() 304 | if e != nil { 305 | return "", e 306 | } 307 | return string(b), e 308 | } 309 | 310 | // RenderBytes returns the bytes of rendered template string. Do not send out response. 311 | func (c *CustomController) RenderBytes() ([]byte, error) { 312 | buf, err := c.renderTemplate() 313 | // if the controller has set layout, then first get the tplName's content set the content to the layout 314 | if err == nil && c.Layout != "" { 315 | c.Data["LayoutContent"] = template.HTML(buf.String()) 316 | 317 | if c.LayoutSections != nil { 318 | for sectionName, sectionTpl := range c.LayoutSections { 319 | if sectionTpl == "" { 320 | c.Data[sectionName] = "" 321 | continue 322 | } 323 | buf.Reset() 324 | c.Data["SectionData"] = c.Data[sectionName+"_Data"] 325 | err = web.ExecuteViewPathTemplate(&buf, sectionTpl, c.viewPath(), c.Data) 326 | if err != nil { 327 | return nil, err 328 | } 329 | c.Data[sectionName] = template.HTML(buf.String()) 330 | } 331 | } 332 | 333 | buf.Reset() 334 | err = web.ExecuteViewPathTemplate(&buf, c.Layout, c.viewPath(), c.Data) 335 | } 336 | return buf.Bytes(), err 337 | } 338 | 339 | func (c *CustomController) renderTemplate() (bytes.Buffer, error) { 340 | var buf bytes.Buffer 341 | if c.TplName == "" { 342 | c.TplName = strings.ToLower(c.controllerName) + "/" + strings.ToLower(c.actionName) + "." + c.TplExt 343 | } 344 | if c.TplPrefix != "" { 345 | c.TplName = c.TplPrefix + c.TplName 346 | } 347 | if web.BConfig.RunMode == web.DEV { 348 | buildFiles := []string{c.TplName} 349 | if c.Layout != "" { 350 | buildFiles = append(buildFiles, c.Layout) 351 | if c.LayoutSections != nil { 352 | for _, sectionTpl := range c.LayoutSections { 353 | if sectionTpl == "" { 354 | continue 355 | } 356 | buildFiles = append(buildFiles, sectionTpl) 357 | } 358 | } 359 | } 360 | web.BuildTemplate(c.viewPath(), buildFiles...) 361 | } 362 | return buf, web.ExecuteViewPathTemplate(&buf, c.TplName, c.viewPath(), c.Data) 363 | } 364 | 365 | func (c *CustomController) viewPath() string { 366 | if c.ViewPath == "" { 367 | return web.BConfig.WebConfig.ViewsPath 368 | } 369 | return c.ViewPath 370 | } 371 | 372 | // Redirect sends the redirection response to url with status code. 373 | func (c *CustomController) Redirect(url string, code int) { 374 | web.LogAccess(c.Ctx, nil, code) 375 | c.Ctx.Redirect(code, url) 376 | } 377 | 378 | // SetData set the data depending on the accepted 379 | func (c *CustomController) SetData(data interface{}) { 380 | accept := c.Ctx.Input.Header("Accept") 381 | switch accept { 382 | case context.ApplicationYAML: 383 | c.Data["yaml"] = data 384 | case context.ApplicationXML, context.TextXML: 385 | c.Data["xml"] = data 386 | default: 387 | c.Data["json"] = data 388 | } 389 | } 390 | 391 | // Abort stops controller handler and show the error data if code is defined in ErrorMap or code string. 392 | func (c *CustomController) Abort(code string) { 393 | status, err := strconv.Atoi(code) 394 | if err != nil { 395 | status = 200 396 | } 397 | c.CustomAbort(status, code) 398 | } 399 | 400 | // CustomAbort stops controller handler and show the error data, it's similar Aborts, but support status code and body. 401 | func (c *CustomController) CustomAbort(status int, body string) { 402 | c.Ctx.Output.Status = status 403 | // first panic from ErrorMaps, it is user defined error functions. 404 | if _, ok := web.ErrorMaps[body]; ok { 405 | panic(body) 406 | } 407 | // last panic user string 408 | c.Ctx.ResponseWriter.WriteHeader(status) 409 | c.Ctx.ResponseWriter.Write([]byte(body)) 410 | panic(ErrAbort) 411 | } 412 | 413 | // StopRun makes panic of USERSTOPRUN error and go to recover function if defined. 414 | func (c *CustomController) StopRun() { 415 | panic(ErrAbort) 416 | } 417 | 418 | // URLFor does another controller handler in this request function. 419 | // it goes to this controller method if endpoint is not clear. 420 | func (c *CustomController) URLFor(endpoint string, values ...interface{}) string { 421 | if len(endpoint) == 0 { 422 | return "" 423 | } 424 | if endpoint[0] == '.' { 425 | return web.URLFor(reflect.Indirect(reflect.ValueOf(c.AppController)).Type().Name()+endpoint, values...) 426 | } 427 | return web.URLFor(endpoint, values...) 428 | } 429 | 430 | func (c *CustomController) JSONResp(data interface{}) error { 431 | return c.Ctx.JSONResp(data) 432 | } 433 | 434 | func (c *CustomController) XMLResp(data interface{}) error { 435 | return c.Ctx.XMLResp(data) 436 | } 437 | 438 | func (c *CustomController) YamlResp(data interface{}) error { 439 | return c.Ctx.YamlResp(data) 440 | } 441 | 442 | // Resp sends response based on the Accept Header 443 | // By default response will be in JSON 444 | // it's different from ServeXXX methods 445 | // because we don't store the data to Data field 446 | func (c *CustomController) Resp(data interface{}) error { 447 | return c.Ctx.Resp(data) 448 | } 449 | 450 | // ServeJSON sends a json response with encoding charset. 451 | func (c *CustomController) ServeJSON(encoding ...bool) error { 452 | var ( 453 | hasIndent = web.BConfig.RunMode != web.PROD 454 | hasEncoding = len(encoding) > 0 && encoding[0] 455 | ) 456 | 457 | return c.Ctx.Output.JSON(c.Data["json"], hasIndent, hasEncoding) 458 | } 459 | 460 | // ServeJSONP sends a jsonp response. 461 | func (c *CustomController) ServeJSONP() error { 462 | hasIndent := web.BConfig.RunMode != web.PROD 463 | return c.Ctx.Output.JSONP(c.Data["jsonp"], hasIndent) 464 | } 465 | 466 | // ServeXML sends xml response. 467 | func (c *CustomController) ServeXML() error { 468 | hasIndent := web.BConfig.RunMode != web.PROD 469 | return c.Ctx.Output.XML(c.Data["xml"], hasIndent) 470 | } 471 | 472 | // ServeYAML sends yaml response. 473 | func (c *CustomController) ServeYAML() error { 474 | return c.Ctx.Output.YAML(c.Data["yaml"]) 475 | } 476 | 477 | // ServeFormatted serve YAML, XML OR JSON, depending on the value of the Accept header 478 | func (c *CustomController) ServeFormatted(encoding ...bool) error { 479 | hasIndent := web.BConfig.RunMode != web.PROD 480 | hasEncoding := len(encoding) > 0 && encoding[0] 481 | return c.Ctx.Output.ServeFormatted(c.Data, hasIndent, hasEncoding) 482 | } 483 | 484 | // Input returns the input data map from POST or PUT request body and query string. 485 | func (c *CustomController) Input() (url.Values, error) { 486 | if c.Ctx.Request.Form == nil { 487 | err := c.Ctx.Request.ParseForm() 488 | if err != nil { 489 | return nil, err 490 | } 491 | } 492 | return c.Ctx.Request.Form, nil 493 | } 494 | 495 | // ParseForm maps input data map to obj struct. 496 | func (c *CustomController) ParseForm(obj interface{}) error { 497 | return c.Ctx.BindForm(obj) 498 | } 499 | 500 | // GetString returns the input value by key string or the default value while it's present and input is blank 501 | func (c *CustomController) GetString(key string, def ...string) string { 502 | if v := c.Ctx.Input.Query(key); v != "" { 503 | return v 504 | } 505 | if len(def) > 0 { 506 | return def[0] 507 | } 508 | return "" 509 | } 510 | 511 | // GetStrings returns the input string slice by key string or the default value while it's present and input is blank 512 | // it's designed for multi-value input field such as checkbox(input[type=checkbox]), multi-selection. 513 | func (c *CustomController) GetStrings(key string, def ...[]string) []string { 514 | var defv []string 515 | if len(def) > 0 { 516 | defv = def[0] 517 | } 518 | 519 | if f, err := c.Input(); f == nil || err != nil { 520 | return defv 521 | } else if vs := f[key]; len(vs) > 0 { 522 | return vs 523 | } 524 | 525 | return defv 526 | } 527 | 528 | // GetInt returns input as an int or the default value while it's present and input is blank 529 | func (c *CustomController) GetInt(key string, def ...int) (int, error) { 530 | strv := c.Ctx.Input.Query(key) 531 | if len(strv) == 0 && len(def) > 0 { 532 | return def[0], nil 533 | } 534 | return strconv.Atoi(strv) 535 | } 536 | 537 | // GetInt8 return input as an int8 or the default value while it's present and input is blank 538 | func (c *CustomController) GetInt8(key string, def ...int8) (int8, error) { 539 | strv := c.Ctx.Input.Query(key) 540 | if len(strv) == 0 && len(def) > 0 { 541 | return def[0], nil 542 | } 543 | i64, err := strconv.ParseInt(strv, 10, 8) 544 | return int8(i64), err 545 | } 546 | 547 | // GetUint8 return input as an uint8 or the default value while it's present and input is blank 548 | func (c *CustomController) GetUint8(key string, def ...uint8) (uint8, error) { 549 | strv := c.Ctx.Input.Query(key) 550 | if len(strv) == 0 && len(def) > 0 { 551 | return def[0], nil 552 | } 553 | u64, err := strconv.ParseUint(strv, 10, 8) 554 | return uint8(u64), err 555 | } 556 | 557 | // GetInt16 returns input as an int16 or the default value while it's present and input is blank 558 | func (c *CustomController) GetInt16(key string, def ...int16) (int16, error) { 559 | strv := c.Ctx.Input.Query(key) 560 | if len(strv) == 0 && len(def) > 0 { 561 | return def[0], nil 562 | } 563 | i64, err := strconv.ParseInt(strv, 10, 16) 564 | return int16(i64), err 565 | } 566 | 567 | // GetUint16 returns input as an uint16 or the default value while it's present and input is blank 568 | func (c *CustomController) GetUint16(key string, def ...uint16) (uint16, error) { 569 | strv := c.Ctx.Input.Query(key) 570 | if len(strv) == 0 && len(def) > 0 { 571 | return def[0], nil 572 | } 573 | u64, err := strconv.ParseUint(strv, 10, 16) 574 | return uint16(u64), err 575 | } 576 | 577 | // GetInt32 returns input as an int32 or the default value while it's present and input is blank 578 | func (c *CustomController) GetInt32(key string, def ...int32) (int32, error) { 579 | strv := c.Ctx.Input.Query(key) 580 | if len(strv) == 0 && len(def) > 0 { 581 | return def[0], nil 582 | } 583 | i64, err := strconv.ParseInt(strv, 10, 32) 584 | return int32(i64), err 585 | } 586 | 587 | // GetUint32 returns input as an uint32 or the default value while it's present and input is blank 588 | func (c *CustomController) GetUint32(key string, def ...uint32) (uint32, error) { 589 | strv := c.Ctx.Input.Query(key) 590 | if len(strv) == 0 && len(def) > 0 { 591 | return def[0], nil 592 | } 593 | u64, err := strconv.ParseUint(strv, 10, 32) 594 | return uint32(u64), err 595 | } 596 | 597 | // GetInt64 returns input value as int64 or the default value while it's present and input is blank. 598 | func (c *CustomController) GetInt64(key string, def ...int64) (int64, error) { 599 | strv := c.Ctx.Input.Query(key) 600 | if len(strv) == 0 && len(def) > 0 { 601 | return def[0], nil 602 | } 603 | return strconv.ParseInt(strv, 10, 64) 604 | } 605 | 606 | // GetUint64 returns input value as uint64 or the default value while it's present and input is blank. 607 | func (c *CustomController) GetUint64(key string, def ...uint64) (uint64, error) { 608 | strv := c.Ctx.Input.Query(key) 609 | if len(strv) == 0 && len(def) > 0 { 610 | return def[0], nil 611 | } 612 | return strconv.ParseUint(strv, 10, 64) 613 | } 614 | 615 | // GetBool returns input value as bool or the default value while it's present and input is blank. 616 | func (c *CustomController) GetBool(key string, def ...bool) (bool, error) { 617 | strv := c.Ctx.Input.Query(key) 618 | if len(strv) == 0 && len(def) > 0 { 619 | return def[0], nil 620 | } 621 | return strconv.ParseBool(strv) 622 | } 623 | 624 | // GetFloat returns input value as float64 or the default value while it's present and input is blank. 625 | func (c *CustomController) GetFloat(key string, def ...float64) (float64, error) { 626 | strv := c.Ctx.Input.Query(key) 627 | if len(strv) == 0 && len(def) > 0 { 628 | return def[0], nil 629 | } 630 | return strconv.ParseFloat(strv, 64) 631 | } 632 | 633 | // GetFile returns the file data in file upload field named as key. 634 | // it returns the first one of multi-uploaded files. 635 | func (c *CustomController) GetFile(key string) (multipart.File, *multipart.FileHeader, error) { 636 | return c.Ctx.Request.FormFile(key) 637 | } 638 | 639 | // GetFiles return multi-upload files 640 | // files, err:=c.GetFiles("myfiles") 641 | // 642 | // if err != nil { 643 | // http.Error(w, err.Error(), http.StatusNoContent) 644 | // return 645 | // } 646 | // 647 | // for i, _ := range files { 648 | // //for each fileheader, get a handle to the actual file 649 | // file, err := files[i].Open() 650 | // defer file.Close() 651 | // if err != nil { 652 | // http.Error(w, err.Error(), http.StatusInternalServerError) 653 | // return 654 | // } 655 | // //create destination file making sure the path is writeable. 656 | // dst, err := os.Create("upload/" + files[i].Filename) 657 | // defer dst.Close() 658 | // if err != nil { 659 | // http.Error(w, err.Error(), http.StatusInternalServerError) 660 | // return 661 | // } 662 | // //copy the uploaded file to the destination file 663 | // if _, err := io.Copy(dst, file); err != nil { 664 | // http.Error(w, err.Error(), http.StatusInternalServerError) 665 | // return 666 | // } 667 | // } 668 | func (c *CustomController) GetFiles(key string) ([]*multipart.FileHeader, error) { 669 | if files, ok := c.Ctx.Request.MultipartForm.File[key]; ok { 670 | return files, nil 671 | } 672 | return nil, http.ErrMissingFile 673 | } 674 | 675 | // SaveToFile saves uploaded file to new path. 676 | // it only operates the first one of mutil-upload form file field. 677 | func (c *CustomController) SaveToFile(fromFile, toFile string) error { 678 | buf := copyBufferPool.Get().([]byte) 679 | defer copyBufferPool.Put(buf) 680 | return c.SaveToFileWithBuffer(fromFile, toFile, buf) 681 | } 682 | 683 | type onlyWriter struct { 684 | io.Writer 685 | } 686 | 687 | func (c *CustomController) SaveToFileWithBuffer(fromFile string, toFile string, buf []byte) error { 688 | src, _, err := c.Ctx.Request.FormFile(fromFile) 689 | if err != nil { 690 | return err 691 | } 692 | defer src.Close() 693 | 694 | dst, err := os.OpenFile(toFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, filePerm) 695 | if err != nil { 696 | return err 697 | } 698 | defer dst.Close() 699 | 700 | _, err = io.CopyBuffer(onlyWriter{dst}, src, buf) 701 | return err 702 | } 703 | 704 | // StartSession starts session and load old session data info this controller. 705 | func (c *CustomController) StartSession() session.Store { 706 | if c.CruSession == nil { 707 | c.CruSession = c.Ctx.Input.CruSession 708 | } 709 | return c.CruSession 710 | } 711 | 712 | // SetSession puts value into session. 713 | func (c *CustomController) SetSession(name interface{}, value interface{}) error { 714 | if c.CruSession == nil { 715 | c.StartSession() 716 | } 717 | return c.CruSession.Set(context2.Background(), name, value) 718 | } 719 | 720 | // GetSession gets value from session. 721 | func (c *CustomController) GetSession(name interface{}) interface{} { 722 | if c.CruSession == nil { 723 | c.StartSession() 724 | } 725 | return c.CruSession.Get(context2.Background(), name) 726 | } 727 | 728 | // DelSession removes value from session. 729 | func (c *CustomController) DelSession(name interface{}) error { 730 | if c.CruSession == nil { 731 | c.StartSession() 732 | } 733 | return c.CruSession.Delete(context2.Background(), name) 734 | } 735 | 736 | // SessionRegenerateID regenerates session id for this session. 737 | // the session data have no changes. 738 | func (c *CustomController) SessionRegenerateID() error { 739 | if c.CruSession != nil { 740 | c.CruSession.SessionRelease(context2.Background(), c.Ctx.ResponseWriter) 741 | } 742 | var err error 743 | c.CruSession, err = web.GlobalSessions.SessionRegenerateID(c.Ctx.ResponseWriter, c.Ctx.Request) 744 | c.Ctx.Input.CruSession = c.CruSession 745 | return err 746 | } 747 | 748 | // DestroySession cleans session data and session cookie. 749 | func (c *CustomController) DestroySession() error { 750 | err := c.Ctx.Input.CruSession.Flush(nil) 751 | if err != nil { 752 | return err 753 | } 754 | c.Ctx.Input.CruSession = nil 755 | web.GlobalSessions.SessionDestroy(c.Ctx.ResponseWriter, c.Ctx.Request) 756 | return nil 757 | } 758 | 759 | // IsAjax returns this request is ajax or not. 760 | func (c *CustomController) IsAjax() bool { 761 | return c.Ctx.Input.IsAjax() 762 | } 763 | 764 | // GetSecureCookie returns decoded cookie value from encoded browser cookie values. 765 | func (c *CustomController) GetSecureCookie(Secret, key string) (string, bool) { 766 | return c.Ctx.GetSecureCookie(Secret, key) 767 | } 768 | 769 | // SetSecureCookie puts value into cookie after encoded the value. 770 | func (c *CustomController) SetSecureCookie(Secret, name, value string, others ...interface{}) { 771 | c.Ctx.SetSecureCookie(Secret, name, value, others...) 772 | } 773 | 774 | // XSRFToken creates a CSRF token string and returns. 775 | func (c *CustomController) XSRFToken() string { 776 | if c._xsrfToken == "" { 777 | expire := int64(web.BConfig.WebConfig.XSRFExpire) 778 | if c.XSRFExpire > 0 { 779 | expire = int64(c.XSRFExpire) 780 | } 781 | c._xsrfToken = c.Ctx.XSRFToken(web.BConfig.WebConfig.XSRFKey, expire) 782 | } 783 | return c._xsrfToken 784 | } 785 | 786 | // CheckXSRFCookie checks xsrf token in this request is valid or not. 787 | // the token can provided in request header "X-Xsrftoken" and "X-CsrfToken" 788 | // or in form field value named as "_xsrf". 789 | func (c *CustomController) CheckXSRFCookie() bool { 790 | if !c.EnableXSRF { 791 | return true 792 | } 793 | return c.Ctx.CheckXSRFCookie() 794 | } 795 | 796 | // XSRFFormHTML writes an input field contains xsrf token value. 797 | func (c *CustomController) XSRFFormHTML() string { 798 | return `` 800 | } 801 | 802 | // GetControllerAndAction gets the executing controller name and action name. 803 | func (c *CustomController) GetControllerAndAction() (string, string) { 804 | return c.controllerName, c.actionName 805 | } 806 | -------------------------------------------------------------------------------- /controllers/index.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | type IndexController struct { 4 | BaseController 5 | } 6 | 7 | // Get main page 8 | func (CTRL *IndexController) Get() { 9 | CTRL.ConfigPage("index.html") 10 | CTRL.Data["Website"] = "127.0.0.1:8080" 11 | CTRL.Data["description"] = "Fast and stable CMS" 12 | CTRL.Data["Email"] = "dionyself@gmail.com" 13 | } 14 | -------------------------------------------------------------------------------- /controllers/panel.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | // UserPanelController ... 4 | type UserPanelController struct { 5 | BaseController 6 | } 7 | 8 | // MainView ... 9 | func (CTRL *UserPanelController) MainView() { 10 | CTRL.ConfigPage("user-panel.html") 11 | } 12 | 13 | // VendorPanelController ... 14 | type VendorPanelController struct { 15 | BaseController 16 | } 17 | 18 | // AdminPanelController ... 19 | type AdminPanelController struct { 20 | BaseController 21 | } 22 | -------------------------------------------------------------------------------- /controllers/profile.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/dionyself/golang-cms/models" 7 | ) 8 | 9 | // ProfileController for users 10 | type ProfileController struct { 11 | BaseController 12 | } 13 | 14 | // UserPanelView Display user's homepage 15 | func (CTRL *ProfileController) UserPanelView() { 16 | UID := CTRL.Ctx.Input.Param(":id") 17 | if CTRL.Ctx.Input.Param(":id") == "0" { 18 | CTRL.ConfigPage("user-profile.html") 19 | } else { 20 | UID, err := strconv.Atoi(UID) 21 | if err != nil { 22 | CTRL.Abort("404") 23 | } 24 | CTRL.populateProfileViewData(UID) 25 | CTRL.ConfigPage("profile-view.html") 26 | } 27 | } 28 | 29 | // populateProfileViewData Displays profile by id 30 | func (CTRL *ProfileController) populateProfileViewData(UID int) bool { 31 | db := CTRL.GetDB() 32 | userView := models.User{Id: UID} 33 | db.Read(&userView, "Id") 34 | Permissions := userView.Profile.GetPermissions(CTRL.Data["user"].(models.User)) 35 | // TODO : populate permissions on CTRL.Data 36 | _ = Permissions 37 | CTRL.Data["Profile"] = userView.Profile 38 | return true 39 | } 40 | -------------------------------------------------------------------------------- /controllers/websocket.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | /* 4 | import ( 5 | "fmt" 6 | "strconv" 7 | "github.com/dionyself/golang-cms/models" 8 | "encoding/json" 9 | "net/http" 10 | "github.com/beego/beego/v2/server/web" 11 | "github.com/dionyself/websocket" 12 | ) 13 | 14 | // WebSocketController handles WebSocket requests. 15 | type WebSocketController struct { 16 | baseController 17 | } 18 | 19 | // Get method handles GET requests for WebSocketController. 20 | func (this *WebSocketController) Get() { 21 | // Safe check. 22 | uname := this.GetString("uname") 23 | if len(uname) == 0 { 24 | this.Redirect("/", 302) 25 | return 26 | } 27 | 28 | this.TplName = "websocket.html" 29 | this.Data["IsWebSocket"] = true 30 | this.Data["UserName"] = uname 31 | } 32 | 33 | // Join method handles WebSocket requests for WebSocketController. 34 | func (this *WebSocketController) Join() { 35 | uname := this.GetString("uname") 36 | if len(uname) == 0 { 37 | this.Redirect("/", 302) 38 | return 39 | } 40 | 41 | // Upgrade from http request to WebSocket. 42 | ws, err := websocket.Upgrade(this.Ctx.ResponseWriter, this.Ctx.Request, nil, 1024, 1024) 43 | if _, ok := err.(websocket.HandshakeError); ok { 44 | http.Error(this.Ctx.ResponseWriter, "Not a websocket handshake", 400) 45 | return 46 | } else if err != nil { 47 | beego.Error("Cannot setup WebSocket connection:", err) 48 | return 49 | } 50 | 51 | // Join chat room. 52 | Join(uname, ws) 53 | defer Leave(uname) 54 | 55 | // Message receive loop. 56 | for { 57 | _, p, err := ws.ReadMessage() 58 | if err != nil { 59 | return 60 | } 61 | publish <- newEvent(models.EVENT_MESSAGE, uname, string(p)) 62 | } 63 | } 64 | 65 | // broadcastWebSocket broadcasts messages to WebSocket users. 66 | func broadcastWebSocket(event models.Event) { 67 | data, err := json.Marshal(event) 68 | if err != nil { 69 | beego.Error("Fail to marshal event:", err) 70 | return 71 | } 72 | 73 | for sub := subscribers.Front(); sub != nil; sub = sub.Next() { 74 | // Immediately send event to WebSocket users. 75 | ws := sub.Value.(Subscriber).Conn 76 | if ws != nil { 77 | if ws.WriteMessage(websocket.TextMessage, data) != nil { 78 | // User disconnected. 79 | unsubscribe <- sub.Value.(Subscriber).Name 80 | } 81 | } 82 | } 83 | } 84 | */ 85 | -------------------------------------------------------------------------------- /core/block/base.go: -------------------------------------------------------------------------------- 1 | package block 2 | 3 | import ( 4 | _ "github.com/beego/beego/v2/server/web" 5 | "github.com/dionyself/golang-cms/core/lib/cache" 6 | "github.com/dionyself/golang-cms/core/lib/db" 7 | "github.com/dionyself/golang-cms/models" 8 | _ "github.com/go-sql-driver/mysql" 9 | _ "github.com/lib/pq" 10 | _ "github.com/mattn/go-sqlite3" 11 | ) 12 | 13 | type Block interface { 14 | GetTemplatePath() string 15 | GetContent() map[string]string 16 | GetPosition() int 17 | GetBlockType() string 18 | //IsContentCacheable() bool 19 | //GetConfig() models.BlockConfig 20 | //Register() 21 | Init() 22 | Load(*models.Block) Block 23 | //SetConfig() bool 24 | //Activate() 25 | //Deactivate() 26 | IsActive() bool 27 | //Index() int 28 | //GetName() string 29 | } 30 | 31 | // Blocks map["block_folder"]["style1", "style2" ...] 32 | var Blocks map[string]Block 33 | var RegisteredBlocks map[string]Block 34 | 35 | // GetBlock get a list of active block (by name) 36 | func GetActiveBlocks(forced bool) []string { 37 | blocks := []string{} 38 | if !forced { 39 | for blockName, currentBlock := range Blocks { 40 | if currentBlock.IsActive() { 41 | blocks = append(blocks, blockName) 42 | } 43 | } 44 | go cache.MainCache.Set("activeBlocks", blocks, 60) 45 | } else { 46 | blocks = cache.MainCache.GetStringList("activeBlocks", 60) 47 | } 48 | return blocks 49 | } 50 | 51 | // initBlock use this to initialize your block 52 | func initBlock(blockToInit Block) { 53 | BlockType := blockToInit.GetBlockType() 54 | DB := db.MainDatabase.GetOrm("") 55 | byTypeBlocks := []models.Block{} 56 | qs := DB.QueryTable("block").Filter("type", BlockType) 57 | qs.All(&byTypeBlocks) 58 | RegisteredBlocks[BlockType] = blockToInit //we may to want populate with some default info 59 | for _, currentBlock := range byTypeBlocks { 60 | Blocks[currentBlock.Name] = blockToInit.Load(¤tBlock) 61 | } 62 | } 63 | 64 | func init() { 65 | RegisteredBlocks = make(map[string]Block) 66 | Blocks = make(map[string]Block) 67 | for _, currentBlock := range Blocks { 68 | currentBlock.Init() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /core/block/html.go: -------------------------------------------------------------------------------- 1 | package block 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | _ "github.com/beego/beego/v2/client/orm" 7 | _ "github.com/beego/beego/v2/server/web" 8 | "github.com/dionyself/golang-cms/models" 9 | _ "github.com/go-sql-driver/mysql" 10 | _ "github.com/lib/pq" 11 | _ "github.com/mattn/go-sqlite3" 12 | ) 13 | 14 | type htmlBlock struct { 15 | content map[string]string 16 | Type string 17 | Name string 18 | Config []*models.BlockConfig //map[string]string 19 | isActive bool 20 | IsCacheable bool 21 | index int 22 | templates []string 23 | template string 24 | position int 25 | } 26 | 27 | // this maybe moved to single funct 28 | // func (block htmlBlock) Init(siteData map[string]string, config map[string]string, content map[string]string) { 29 | func (block htmlBlock) Init() { 30 | // blockModel := new(models.Block) 31 | // blockModel.Config = block.generateConfig(siteData map[string]string, config map[string]string) 32 | // blockModel.Content = block.generateContent(configModel, content map[string]string) 33 | // Block[config["name"]] = block.load(models.Block) 34 | } 35 | 36 | // GetBlockType yup this is hardcoded an this the way to do it 37 | func (block htmlBlock) GetBlockType() string { 38 | return "html" 39 | } 40 | 41 | func (block htmlBlock) GetContent() map[string]string { 42 | return block.content 43 | } 44 | func (block htmlBlock) GetPosition() int { 45 | return block.position 46 | } 47 | 48 | func (block htmlBlock) IsActive() bool { 49 | return block.isActive 50 | } 51 | 52 | // this couldbe reimplemented support mutiple themes/templates/style 53 | // by now hardoding 54 | func (block htmlBlock) GetTemplatePath() string { 55 | return "default/blocks/html_block.html" 56 | } 57 | 58 | func (block htmlBlock) Load(blockModel *models.Block) Block { 59 | //block.Type = blockModel.Type 60 | json.Unmarshal([]byte(blockModel.Content), &block.content) 61 | block.Name = blockModel.Name 62 | //block.Config = blockModel.Config 63 | block.position = blockModel.Position 64 | block.isActive = blockModel.IsActive 65 | return block 66 | } 67 | 68 | func init() { 69 | initBlock(htmlBlock{}) 70 | } 71 | -------------------------------------------------------------------------------- /core/defaults/blocks/news.go: -------------------------------------------------------------------------------- 1 | package blocks 2 | -------------------------------------------------------------------------------- /core/defaults/menu.go: -------------------------------------------------------------------------------- 1 | package defaults 2 | 3 | import ( 4 | "github.com/beego/beego/v2/server/web" 5 | "github.com/dionyself/golang-cms/core/defaults/modules" 6 | ) 7 | 8 | // GetDefaultMenu get menu 9 | func GetDefaultMenu() string { 10 | var menuitems string 11 | for _, mod := range modules.Modules { 12 | modConfig, err := web.AppConfig.GetSection("module-" + mod.Name) 13 | if err == nil && modConfig["activated"] != "" && modConfig["hidden"] != "" { 14 | menuitems = mod.Menu 15 | } 16 | } 17 | return menuitems 18 | } 19 | -------------------------------------------------------------------------------- /core/defaults/modules/news.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | // ModuleConfig ... 4 | type ModuleConfig struct { 5 | Name string 6 | Menu string 7 | Weight int 8 | } 9 | 10 | // Modules ... 11 | var Modules []ModuleConfig 12 | 13 | func init() { 14 | var moduleConfig ModuleConfig 15 | moduleConfig.Name = "news" 16 | moduleConfig.Menu = "{'news': '/news', 'top_10': '/news_top'}" 17 | Modules = append(Modules, moduleConfig) 18 | } 19 | -------------------------------------------------------------------------------- /core/lib/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | redisCacheAdapter "github.com/beego/beego/v2/adapter/cache" 9 | redisCache "github.com/beego/beego/v2/client/cache" 10 | _ "github.com/beego/beego/v2/client/cache/redis" 11 | "github.com/beego/beego/v2/server/web" 12 | internalCache "github.com/patrickmn/go-cache" 13 | ) 14 | 15 | // MainCache is global cache 16 | var MainCache CACHE 17 | 18 | // CACHE main ... 19 | type CACHE struct { 20 | isEnabled bool 21 | servers map[string]redisCacheAdapter.Cache 22 | internal *internalCache.Cache 23 | DefaultExpiration time.Duration 24 | dualmode bool 25 | } 26 | 27 | // GetMap bring string from cache, expiration time in seconds 28 | func (cache *CACHE) GetMap(cacheKey string, expirationTime int64) (map[string]string, bool) { 29 | if !cache.isEnabled { 30 | return nil, false 31 | } 32 | var result map[string]string 33 | Map := func(incomingValue interface{}) map[string]string { 34 | switch value := incomingValue.(type) { 35 | case map[string]string: 36 | result = value 37 | case []byte: 38 | var newValue map[string]string 39 | json.Unmarshal(value, &newValue) 40 | result = newValue 41 | case string: 42 | var newValue map[string]string 43 | json.Unmarshal([]byte(value), &newValue) 44 | result = newValue 45 | default: 46 | result = nil 47 | } 48 | return result 49 | } 50 | payload, found := cache.internal.Get(cacheKey) 51 | if found { 52 | result = Map(payload) 53 | if result != nil { 54 | return result, true 55 | } 56 | } 57 | if cache.dualmode { 58 | server := cache.servers["slave"] 59 | slaveResult := server.Get(cacheKey) 60 | if slaveResult != nil { 61 | go cache.Set(cacheKey, slaveResult, expirationTime) 62 | result = Map(slaveResult) 63 | if result != nil { 64 | return result, true 65 | } 66 | } 67 | } 68 | return nil, false 69 | } 70 | 71 | func (cache *CACHE) GetStringList(cacheKey string, expirationTime int64) []string { 72 | return []string{} 73 | } 74 | 75 | // GetString bring string from cache, expiration time in seconds 76 | func (cache *CACHE) GetString(cacheKey string, expirationTime int64) (string, bool) { 77 | if !cache.isEnabled { 78 | return "", false 79 | } 80 | var result string 81 | String := func(incomingValue interface{}) string { 82 | switch value := incomingValue.(type) { 83 | case string: 84 | result = value 85 | case int32, int64: 86 | result = fmt.Sprintf("%v", value) 87 | case []byte: 88 | result = string(value[:]) 89 | case map[string]string: 90 | jsonValue, _ := json.Marshal(value) 91 | result = string(jsonValue[:]) 92 | default: 93 | result = "" 94 | } 95 | return result 96 | } 97 | payload, found := cache.internal.Get(cacheKey) 98 | if found { 99 | result = String(payload) 100 | if result != "" { 101 | return result, true 102 | } 103 | } 104 | if cache.dualmode { 105 | server := cache.servers["slave"] 106 | slaveResult := server.Get(cacheKey) 107 | if slaveResult != nil { 108 | go cache.Set(cacheKey, slaveResult, expirationTime) 109 | result = String(slaveResult) 110 | if result != "" { 111 | return result, true 112 | } 113 | } 114 | } 115 | return "", false 116 | } 117 | 118 | // Set any value to cache, expiration time in seconds 119 | func (cache *CACHE) Set(cacheKey string, value interface{}, expirationTime int64) bool { 120 | if !cache.isEnabled { 121 | return false 122 | } 123 | duration := cache.DefaultExpiration 124 | if expirationTime != 0 { 125 | duration = time.Duration(expirationTime) * time.Second 126 | } else { 127 | duration = cache.DefaultExpiration 128 | } 129 | cache.internal.Set(cacheKey, value, duration) 130 | if cache.dualmode { 131 | server := cache.servers["master"] 132 | err := server.Put(cacheKey, value, duration) 133 | if err == nil { 134 | return true 135 | } 136 | } 137 | return true 138 | } 139 | 140 | func init() { 141 | env, _ := web.AppConfig.String("RunMode") 142 | cacheBlk := "cacheConfig-" + env + "::" 143 | isEnable, _ := web.AppConfig.Bool(cacheBlk + "enabled") 144 | dualmode, _ := web.AppConfig.Bool(cacheBlk + "dualmode") 145 | master, _ := web.AppConfig.String(cacheBlk + "redisMasterServer") 146 | slave, _ := web.AppConfig.String(cacheBlk + "redisSlaveServer") 147 | flushInterval, _ := web.AppConfig.Int64(cacheBlk + "flushInterval") 148 | defaultExpiry, _ := web.AppConfig.Int64(cacheBlk + "defaultExpiry") 149 | MainCache = CACHE{isEnabled: isEnable, dualmode: dualmode} 150 | MainCache.internal = internalCache.New(time.Duration(defaultExpiry)*time.Second, time.Duration(flushInterval)*time.Second) 151 | MainCache.DefaultExpiration = internalCache.DefaultExpiration 152 | MainCache.servers = make(map[string]redisCacheAdapter.Cache) 153 | if dualmode { 154 | masterRedis, _ := redisCache.NewCache("redis", master) 155 | slaveRedis, _ := redisCache.NewCache("redis", slave) 156 | MainCache.servers["slave"] = redisCacheAdapter.CreateNewToOldCacheAdapter(slaveRedis) 157 | MainCache.servers["master"] = redisCacheAdapter.CreateNewToOldCacheAdapter(masterRedis) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /core/lib/db/data.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/dionyself/golang-cms/models" 5 | "github.com/dionyself/golang-cms/utils" 6 | ) 7 | 8 | // InsertDemoData insert demo data in database if ReCreateDatabase is true 9 | func InsertDemoData() bool { 10 | db := MainDatabase.GetOrm("default") 11 | category := models.Category{Name: "Generic"} 12 | user := models.User{Username: "test"} 13 | salt := utils.GetRandomString(10) 14 | encodedPassword := salt + "$" + utils.EncodePassword("test", salt) 15 | profile := new(models.Profile) 16 | profile.Age = 30 17 | profile.Name = "Test Rosario" 18 | profile.Avatar = "male" 19 | profile.Description = "Hi, Please insert here a litte description about you. this is just a demo." 20 | user.Profile = profile 21 | db.Insert(profile) 22 | user.Password = encodedPassword 23 | user.Rands = salt 24 | article := models.Article{ 25 | Title: "This is an example of article!", 26 | Content: "

Sabemos que los ácaros sólo pueden sobrevivir mediante la ingestión de agua de la atmósfera, utilizando glándulas pequeñas en el exterior de su cuerpo. Algo tan simple como dejar la cama sin hacer durante el día puede eliminar la humedad de las sábanas y el colchón, provocando que en consecuencia, los ácaros se deshidraten y finalmente mueran.

A subscription costs just £1 GBP / $1.60 USD per device per month (volume pricing is available).
We offer a free 14 day trial with no obligation to subscribe once the trial ends.

PRO features are included for the life of the subscription.

*Subscriptions are re-billed every 30 days and you will be charged for the amount of devices registered to your account. Annual subscriptions require an upfront payment.
", 27 | Category: &category, 28 | User: &user} 29 | db.Insert(&user) 30 | db.Insert(&category) 31 | db.Insert(&article) 32 | htmlblock := models.Block{Name: "Default html block1", Type: "html", IsActive: true, Position: 1, Content: "{\"body\": \"this is a test for default blocks position 1 !\"}"} 33 | db.Insert(&htmlblock) 34 | htmlblock = models.Block{Name: "Default html block2", Type: "html", IsActive: true, Position: 2, Content: "{\"body\": \"this is a test for default blocks position 2 !\"}"} 35 | db.Insert(&htmlblock) 36 | 37 | return true 38 | } 39 | -------------------------------------------------------------------------------- /core/lib/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/beego/beego/v2/client/orm" 7 | "github.com/beego/beego/v2/server/web" 8 | _ "github.com/go-sql-driver/mysql" 9 | _ "github.com/lib/pq" 10 | _ "github.com/mattn/go-sqlite3" 11 | ) 12 | 13 | // MainDatabase main db 14 | var MainDatabase DB 15 | 16 | // DB ... 17 | type DB struct { 18 | Pool map[string]orm.Ormer 19 | Replicated bool 20 | } 21 | 22 | func (db *DB) GetOrm(db_name string) orm.Ormer { 23 | if (db_name == "") || (db_name == "master") { 24 | db_name = "default" 25 | } 26 | _, ok := db.Pool[db_name] 27 | if ok { 28 | return db.Pool[db_name] 29 | } 30 | db.Pool[db_name] = orm.NewOrmUsingDB(db_name) 31 | return db.Pool[db_name] 32 | } 33 | 34 | func init() { 35 | fmt.Println("loading utils.db") 36 | env, _ := web.AppConfig.String("RunMode") 37 | dbBlk := "databaseConfig-" + env + "::" 38 | MasterAddress := "" 39 | SlaveAddress := "" 40 | masterServerPort := "" 41 | slaveServerPort := "" 42 | replicated := false 43 | Engine, _ := web.AppConfig.String("DatabaseProvider") 44 | replicated, _ = web.AppConfig.Bool(dbBlk + "replicated") 45 | masterServerPort, _ = web.AppConfig.String(dbBlk + "masterServerPort") 46 | slaveServerPort, _ = web.AppConfig.String(dbBlk + "slaveServerPort") 47 | Username, _ := web.AppConfig.String(dbBlk + "databaseUser") 48 | UserPassword, _ := web.AppConfig.String(dbBlk + "userPassword") 49 | MasterServer, _ := web.AppConfig.String(dbBlk + "masterServer") 50 | SlaveServer, _ := web.AppConfig.String(dbBlk + "slaveServer") 51 | Name, _ := web.AppConfig.String(dbBlk + "databaseName") 52 | maxIdle := 300 53 | maxConn := 300 54 | if Engine == "" { 55 | panic("Engine not configured valid options: sqlite3, mysql or postgres") 56 | } 57 | if Engine != "sqlite3" && MasterServer == "" { 58 | panic("masterServer not configured") 59 | } 60 | if replicated == true && SlaveServer == "" { 61 | panic("DB Replication: slaveServer not configured") 62 | } 63 | if Engine == "mysql" { 64 | orm.RegisterDriver(Engine, orm.DRMySQL) 65 | if masterServerPort == "0" { 66 | masterServerPort = "3306" 67 | } 68 | if slaveServerPort == "0" { 69 | slaveServerPort = "3306" 70 | } 71 | MasterAddress = Username + ":" + UserPassword + "@tcp(" + MasterServer + ":" + masterServerPort + ")/" + Name + "?charset=utf8" 72 | if replicated == true { 73 | SlaveAddress = Username + ":" + UserPassword + "@tcp(" + SlaveServer + ":" + slaveServerPort + ")/" + Name + "?charset=utf8" 74 | } 75 | } else if Engine == "sqlite3" { 76 | orm.RegisterDriver(Engine, orm.DRSqlite) 77 | fl_c, _ := web.AppConfig.String(dbBlk + "sqliteFile") 78 | MasterAddress = "file:" + fl_c 79 | } else if Engine == "postgres" { 80 | orm.RegisterDriver(Engine, orm.DRPostgres) 81 | if masterServerPort == "0" { 82 | masterServerPort = "5432" 83 | } 84 | if slaveServerPort == "0" { 85 | slaveServerPort = "5432" 86 | } 87 | MasterAddress = "user=" + Username + " password=" + UserPassword + " dbname=" + Name + " host=" + MasterServer + " port=" + masterServerPort + " sslmode=disable" 88 | if replicated == true { 89 | SlaveAddress = "user=" + Username + " password=" + UserPassword + " dbname=" + Name + " host=" + SlaveServer + " port=" + slaveServerPort + " sslmode=disable" 90 | } 91 | } 92 | err := orm.RegisterDataBase( 93 | "default", 94 | Engine, 95 | MasterAddress, 96 | orm.MaxIdleConnections(maxIdle), 97 | orm.MaxOpenConnections(maxConn)) 98 | if err != nil { 99 | panic("DB: cannot register DB on master") 100 | } else if replicated == true && Engine != "sqlite3" { 101 | if MasterAddress == SlaveAddress { 102 | panic("DB Replication: MasterAddress and SlaveAddress are equal!") 103 | } 104 | err = orm.RegisterDataBase( 105 | "slave", 106 | Engine, 107 | SlaveAddress, 108 | orm.MaxIdleConnections(maxIdle), 109 | orm.MaxOpenConnections(maxConn)) 110 | } 111 | 112 | // DB SETUP 113 | orm.Debug, _ = web.AppConfig.Bool("DatabaseDebugMode") 114 | force, _ := web.AppConfig.Bool("ReCreateDatabase") 115 | verbose, _ := web.AppConfig.Bool("DatabaseLogging") 116 | err = orm.RunSyncdb("default", force, verbose) 117 | if err != nil { 118 | panic(err) 119 | } else if replicated == true && Engine != "sqlite3" { 120 | err = orm.RunSyncdb("slave", force, verbose) 121 | } 122 | if err != nil { 123 | panic(err) 124 | } 125 | MainDatabase = DB{} 126 | MainDatabase.Pool = make(map[string]orm.Ormer) 127 | MainDatabase.Replicated = (replicated == true && Engine != "sqlite3") 128 | 129 | insertDemo, _ := web.AppConfig.Bool(dbBlk + "insertDemoData") 130 | if force && insertDemo { 131 | InsertDemoData() 132 | } 133 | 134 | if MainDatabase.Replicated == true { 135 | MainDatabase.GetOrm("slave") 136 | MainDatabase.Pool["slave"].Raw("start slave") 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /core/template/path.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "io/ioutil" 5 | "strings" 6 | 7 | _ "github.com/beego/beego/v2/client/orm" 8 | _ "github.com/beego/beego/v2/server/web" 9 | "github.com/dionyself/golang-cms/core/lib/cache" 10 | "github.com/dionyself/golang-cms/core/lib/db" 11 | "github.com/dionyself/golang-cms/models" 12 | "github.com/dionyself/golang-cms/utils" 13 | _ "github.com/go-sql-driver/mysql" 14 | _ "github.com/lib/pq" 15 | _ "github.com/mattn/go-sqlite3" 16 | ) 17 | 18 | // Templates map["theme_folder"]["style1", "style2" ...] 19 | var Templates map[string][]string 20 | 21 | // GetActiveTheme gets active theme (cached) 22 | func GetActiveTheme(forceDatabase bool) []string { 23 | theme := []string{"default", "default"} 24 | if value, err := cache.MainCache.GetString("activeTheme", 60); err != false { 25 | if !forceDatabase { 26 | return strings.Split(value, ":") 27 | } 28 | theme = strings.Split(value, ":") 29 | } 30 | template := new(models.Template) 31 | template.Active = true 32 | err := db.MainDatabase.GetOrm("").Read(template, "Active") 33 | if err == nil { 34 | theme[0] = template.Name 35 | for _, style := range template.Style { 36 | if style.Active { 37 | theme[1] = style.Name 38 | } 39 | } 40 | go cache.MainCache.Set("activeTheme", strings.Join(theme, ":"), 60) 41 | } 42 | return theme 43 | } 44 | 45 | // SetActiveTheme ... 46 | func SetActiveTheme(themeToSet []string) bool { 47 | activeTheme := GetActiveTheme(true) 48 | template := new(models.Template) 49 | template.Name = themeToSet[0] 50 | if db.MainDatabase.GetOrm("").Read(&template, "Name") == nil { 51 | template.Active = true 52 | tOrm, _ := db.MainDatabase.GetOrm("").Begin() 53 | if _, err := tOrm.Update(template, "Active"); err == nil { 54 | toDeactivate := new(models.Template) 55 | toDeactivate.Name = activeTheme[0] 56 | toDeactivate.Active = true 57 | if tOrm.Read(&toDeactivate, "Name", "Active") == nil { 58 | toDeactivate.Active = false 59 | if _, err := tOrm.Update(&toDeactivate, "Active"); err != nil { 60 | tOrm.Rollback() 61 | return false 62 | } 63 | } 64 | } else { 65 | tOrm.Rollback() 66 | return false 67 | } 68 | if err := tOrm.Commit(); err == nil { 69 | for _, style := range template.Style { 70 | if style.Name == themeToSet[1] { 71 | style.Active = true 72 | } else { 73 | style.Active = false 74 | } 75 | tOrm.Update(&style, "Active") 76 | } 77 | go cache.MainCache.Set("activeTheme", strings.Join(themeToSet, ":"), 60) 78 | return true 79 | } 80 | } 81 | return false 82 | } 83 | 84 | // SaveTemplates save loaded templates into db, thi usually runs on startup 85 | func SaveTemplates() { 86 | db := db.MainDatabase.GetOrm("default") 87 | var templates []*models.Template 88 | db.QueryTable("template").All(&templates) 89 | var existing_templates []string 90 | var hasActiveTemplate bool 91 | var hasActiveStyle bool 92 | for _, theme := range templates { 93 | if hasActiveTemplate { 94 | theme.Active = false 95 | } 96 | if theme.Active { 97 | hasActiveTemplate = true 98 | } 99 | if _, ok := Templates[theme.Name]; ok { 100 | var existing_styles []string 101 | for _, style := range theme.Style { 102 | if hasActiveStyle { 103 | style.Active = false 104 | } 105 | if style.Active { 106 | hasActiveStyle = true 107 | } 108 | if !utils.Contains(Templates[theme.Name], style.Name) { 109 | db.Delete(&style) 110 | } else { 111 | existing_styles = append(existing_styles, style.Name) 112 | } 113 | } 114 | for _, style := range Templates[theme.Name] { 115 | if !utils.Contains(existing_styles, style) { 116 | mstyle := models.Style{Name: style, Template: theme} 117 | db.Insert(&mstyle) 118 | } 119 | } 120 | 121 | existing_templates = append(existing_templates, theme.Name) 122 | } else { 123 | db.Delete(&theme) 124 | } 125 | } 126 | for template, styles := range Templates { 127 | if !utils.Contains(existing_templates, template) { 128 | mtemplate := models.Template{Name: template} 129 | if mtemplate.Name == "default" && !hasActiveTemplate { 130 | mtemplate.Active = true 131 | } 132 | db.Insert(&mtemplate) 133 | for _, stl := range styles { 134 | mstyle := models.Style{Name: stl, Template: &mtemplate} 135 | if mstyle.Name == "default" && !hasActiveStyle { 136 | mstyle.Active = true 137 | } 138 | db.Insert(&mstyle) 139 | } 140 | } 141 | } 142 | } 143 | 144 | // LoadTemplates this usually runs on startup 145 | func LoadTemplates() { 146 | templates, _ := ioutil.ReadDir("./views/") 147 | Templates = make(map[string][]string) 148 | for _, t := range templates { 149 | if t.IsDir() { 150 | styles, _ := ioutil.ReadDir("./views/" + t.Name() + "/styles/") 151 | Templates[t.Name()] = make([]string, len(styles)-1) 152 | for _, s := range styles { 153 | if s.IsDir() { 154 | Templates[t.Name()] = append(Templates[t.Name()], s.Name()) 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | func init() { 162 | LoadTemplates() 163 | SaveTemplates() 164 | } 165 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dionyself/golang-cms 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/Shaked/gomobiledetect v0.0.0-20171211181707-25f014f66568 7 | github.com/beego/beego/v2 v2.0.7 8 | github.com/dionyself/cutter v0.2.0 9 | github.com/go-sql-driver/mysql v1.7.0 10 | github.com/lib/pq v1.10.7 11 | github.com/mattn/go-sqlite3 v1.14.16 12 | github.com/patrickmn/go-cache v2.1.0+incompatible 13 | github.com/smartystreets/goconvey v1.7.2 14 | ) 15 | 16 | require ( 17 | github.com/beorn7/perks v1.0.1 // indirect 18 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 19 | github.com/go-redis/redis/v7 v7.4.1 // indirect 20 | github.com/golang/protobuf v1.5.2 // indirect 21 | github.com/gomodule/redigo v2.0.0+incompatible // indirect 22 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect 23 | github.com/gorilla/context v1.1.1 // indirect 24 | github.com/hashicorp/golang-lru v0.5.4 // indirect 25 | github.com/jtolds/gls v4.20.0+incompatible // indirect 26 | github.com/kr/pretty v0.2.1 // indirect 27 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 28 | github.com/mitchellh/mapstructure v1.5.0 // indirect 29 | github.com/onsi/ginkgo v1.12.0 // indirect 30 | github.com/onsi/gomega v1.7.1 // indirect 31 | github.com/pkg/errors v0.9.1 // indirect 32 | github.com/prometheus/client_golang v1.14.0 // indirect 33 | github.com/prometheus/client_model v0.3.0 // indirect 34 | github.com/prometheus/common v0.39.0 // indirect 35 | github.com/prometheus/procfs v0.9.0 // indirect 36 | github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect 37 | github.com/smartystreets/assertions v1.2.0 // indirect 38 | golang.org/x/crypto v0.5.0 // indirect 39 | golang.org/x/net v0.5.0 // indirect 40 | golang.org/x/sys v0.4.0 // indirect 41 | golang.org/x/text v0.6.0 // indirect 42 | google.golang.org/protobuf v1.28.1 // indirect 43 | gopkg.in/yaml.v2 v2.4.0 // indirect 44 | gopkg.in/yaml.v3 v3.0.1 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Shaked/gomobiledetect v0.0.0-20171211181707-25f014f66568 h1:p9o1sZ/DTGr7HbztLxf7Z5yc6ymGWJHTOK4fCNlHDa0= 2 | github.com/Shaked/gomobiledetect v0.0.0-20171211181707-25f014f66568/go.mod h1:5Yu1YVbQZw+wQLwJqyxXA6MYOGukCoXszWTw/S8dWnY= 3 | github.com/beego/beego/v2 v2.0.7 h1:9KNnUM40tn3pbCOFfe6SJ1oOL0oTi/oBS/C/wCEdAXA= 4 | github.com/beego/beego/v2 v2.0.7/go.mod h1:f0uOEkmJWgAuDTlTxUdgJzwG3PDSIf3UWF3NpMohbFE= 5 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 6 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 7 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 8 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/dionyself/cutter v0.2.0 h1:xJvsk/iqpEgRwx4xGz+urD+AaaRtCD9jyazde7rXioc= 11 | github.com/dionyself/cutter v0.2.0/go.mod h1:kEoRcfBM/gMm7iVNLJKZhev6kFi94EPSn7DlCcIXT78= 12 | github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw= 13 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 14 | github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= 15 | github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= 16 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 17 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 18 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 19 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 20 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 21 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 22 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 23 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 24 | github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= 25 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 26 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 27 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 28 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 29 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 30 | github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 31 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 32 | github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= 33 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 34 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 35 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 36 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 37 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 38 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 39 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 40 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 41 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 42 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 43 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 44 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= 45 | github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 46 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 47 | github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 48 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 49 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 50 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 51 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 52 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 53 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 54 | github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= 55 | github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= 56 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 57 | github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ= 58 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 59 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 60 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 61 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 62 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 63 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 64 | github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= 65 | github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= 66 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 67 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 68 | github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI= 69 | github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y= 70 | github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= 71 | github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= 72 | github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 h1:DAYUYH5869yV94zvCES9F51oYtN5oGlwjxJJz7ZCnik= 73 | github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= 74 | github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 75 | github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 76 | github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= 77 | github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 78 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 79 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 80 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 81 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 82 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 83 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 84 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 85 | golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= 86 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 87 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 88 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 89 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 90 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 91 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 94 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 95 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 96 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 97 | golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= 98 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 99 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 100 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 101 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 102 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 103 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 104 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 105 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 106 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 107 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 108 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 109 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 110 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 111 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 112 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 113 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 114 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 115 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 116 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 117 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 118 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 119 | -------------------------------------------------------------------------------- /integration_tests/base.go: -------------------------------------------------------------------------------- 1 | package integration_tests 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/beego/beego/v2/server/web" 13 | ) 14 | 15 | func init() { 16 | test_env_path, err := os.Getwd() 17 | if err != nil { 18 | fmt.Println(err) 19 | return 20 | } 21 | fmt.Println(test_env_path) 22 | apppath, _ := filepath.Abs(test_env_path) 23 | fmt.Println(apppath) 24 | web.TestBeegoInit(apppath) 25 | web.BConfig.RunMode = "test" 26 | } 27 | 28 | func MakeRequest(method string, url string, headers map[string]string, body []byte, lastResponse *httptest.ResponseRecorder) *httptest.ResponseRecorder { 29 | 30 | bodyReader := bytes.NewReader(body) 31 | r, _ := http.NewRequest(method, url, bodyReader) 32 | for header, value := range headers { 33 | r.Header.Set(header, value) 34 | } 35 | 36 | if lastResponse != nil { 37 | respHeader := lastResponse.Header() 38 | var cookies []string 39 | for _, raw_cookie := range respHeader.Values("set-cookie") { 40 | cookies = append(cookies, strings.Split(raw_cookie, ";")[0]) 41 | } 42 | r.Header.Set("Cookie", strings.Join(append(cookies, r.Header.Values("Cookie")...), "; ")) 43 | } 44 | 45 | w := httptest.NewRecorder() 46 | web.BeeApp.Handlers.ServeHTTP(w, r) 47 | return w 48 | } 49 | -------------------------------------------------------------------------------- /integration_tests/conf/app.conf: -------------------------------------------------------------------------------- 1 | RunMode = "test" 2 | 3 | # -- Application Config -- 4 | appname = golang-cms 5 | SessionOn = true 6 | DefaultDevice = "desktop" 7 | DirectoryIndex = true 8 | CopyRequestBody = true 9 | 10 | [prod] 11 | HttpAddr = "0.0.0.0" 12 | HttpPort = 80 13 | SessionProvider = redis 14 | DatabaseProvider = postgres 15 | ReCreateDatabase = false 16 | DatabaseDebugMode = false 17 | DatabaseLogging = false 18 | 19 | [dev] 20 | HttpAddr = "127.0.0.1" 21 | HttpPort = 8080 22 | SessionProvider = redis 23 | DatabaseProvider = mysql 24 | ReCreateDatabase = true 25 | DatabaseDebugMode = true 26 | DatabaseLogging = true 27 | 28 | [test] 29 | HttpAddr = "0.0.0.0" 30 | HttpPort = 8080 31 | SessionProvider = memory 32 | DatabaseProvider = sqlite3 33 | ReCreateDatabase = true 34 | DatabaseDebugMode = true 35 | DatabaseLogging = true 36 | 37 | include "session.conf" 38 | include "database.conf" 39 | include "cache.conf" 40 | include "storage.conf" 41 | include "modules.conf" 42 | include "fcgi.conf" 43 | -------------------------------------------------------------------------------- /integration_tests/conf/cache.conf: -------------------------------------------------------------------------------- 1 | [cacheConfig-prod] 2 | enabled = true 3 | dualMode = true 4 | flushInterval = 60 5 | defaultExpiry = 120 6 | redisMasterServer = {"key":"golangCMS","conn":"127.0.0.1:6379","dbNum":"0", "password":""} 7 | redisSlaveServer = {"key":"golangCMS","conn":"127.0.0.1:6379","dbNum":"0", "password":""} 8 | redisDatabaseIndex = 0 9 | redisKey = "collectionName" 10 | 11 | [cacheConfig-dev] 12 | enabled = true 13 | dualMode = true 14 | flushInterval = 60 15 | defaultExpiry = 120 16 | redisMasterServer = {"key":"golangCMS","conn":"127.0.0.1:6379","dbNum":"0", "password":""} 17 | redisSlaveServer = {"key":"golangCMS","conn":"127.0.0.1:6379","dbNum":"0", "password":""} 18 | redisDatabaseIndex = 0 19 | redisKey = "collectionName" 20 | 21 | [cacheConfig-test] 22 | enabled = true 23 | dualMode = false 24 | flushInterval = 60 25 | defaultExpiry = 120 26 | redisMasterServer = {"key":"golangCMS","conn":"127.0.0.1:6379","dbNum":"0", "password":""} 27 | redisSlaveServer = {"key":"golangCMS","conn":"127.0.0.1:6379","dbNum":"0", "password":""} 28 | redisDatabaseIndex = 0 29 | redisKey = "collectionName" 30 | -------------------------------------------------------------------------------- /integration_tests/conf/database.conf: -------------------------------------------------------------------------------- 1 | [databaseConfig-prod] 2 | replicaded = true 3 | masterServer = "localhost" 4 | masterServerPort = 0 5 | slaveServer = "localhost" 6 | slaveServerPort = 0 7 | databaseName = golang_cms 8 | databaseUser = golang_cms 9 | userPassword = golang_cms 10 | sqliteFile = data.db 11 | insertDemoData = true 12 | 13 | [databaseConfig-dev] 14 | replicaded = false 15 | masterServer = "localhost" 16 | masterServerPort = 0 17 | slaveServer = "localhost" 18 | slaveServerPort = 0 19 | databaseName = golang_cms 20 | databaseUser = golang_cms 21 | userPassword = golang_cms 22 | sqliteFile = data.db 23 | insertDemoData = true 24 | 25 | [databaseConfig-test] 26 | replicaded = false 27 | masterServer = "localhost" 28 | masterServerPort = 0 29 | slaveServer = "localhost" 30 | slaveServerPort = 0 31 | databaseName = golang_cms 32 | databaseUser = golang_cms 33 | userPassword = golang_cms 34 | sqliteFile = data.db 35 | insertDemoData = true 36 | -------------------------------------------------------------------------------- /integration_tests/conf/fcgi.conf: -------------------------------------------------------------------------------- 1 | # -- FastCGI Config -- 2 | usefcgi = false 3 | #HttpAddr="/tmp/golang-cms.sock" 4 | #HttpPort=0 5 | -------------------------------------------------------------------------------- /integration_tests/conf/modules.conf: -------------------------------------------------------------------------------- 1 | [modulesConfig-prod] 2 | Activated = true 3 | UsesBlocks = true 4 | Hidden = true 5 | 6 | [modulesConfig-dev] 7 | Activated = true 8 | UsesBlocks = true 9 | Hidden = true 10 | 11 | [modulesConfig-test] 12 | Activated = true 13 | UsesBlocks = true 14 | Hidden = true 15 | -------------------------------------------------------------------------------- /integration_tests/conf/nginx-fcgi.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name golang-cms.com; 4 | #root $GOLANG_CMS_STATIC; 5 | 6 | location / { 7 | error_log /var/log/nginx/golang-cms.error.log; 8 | access_log /var/log/nginx/golang-cms.log; 9 | include fastcgi_params; 10 | fastcgi_pass unix:/tmp/golang-cms.sock; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /integration_tests/conf/session.conf: -------------------------------------------------------------------------------- 1 | [sessionConfig-prod] 2 | memcacheServer = "127.0.0.1:11211" 3 | redisServer = "127.0.0.1:6379,100,password" 4 | memoryConfig = "" 5 | cookieName = "gosessionid" 6 | enableSetCookie = true 7 | omitempty = true 8 | gclifetime = 3600 9 | maxLifetime = 3600 10 | secure = false 11 | sessionIDHashFunc = sha1 12 | 13 | [sessionConfig-dev] 14 | redisServer = "127.0.0.1:6379,100" 15 | memoryConfig = "" 16 | cookieName = "gosessionid" 17 | enableSetCookie = true 18 | gclifetime = 3600 19 | maxLifetime = 3600 20 | secure = false 21 | sessionIDHashFunc = sha1 22 | 23 | [sessionConfig-test] 24 | memcacheServer = "127.0.0.1:11211" 25 | redisServer = "127.0.0.1:6379,100" 26 | memoryConfig = "" 27 | cookieName = "gosessionid" 28 | enableSetCookie = true 29 | omitempty = true 30 | gclifetime = 3600 31 | maxLifetime = 3600 32 | secure = false 33 | sessionIDHashFunc = sha1 34 | -------------------------------------------------------------------------------- /integration_tests/conf/storage.conf: -------------------------------------------------------------------------------- 1 | [localStorageConfig-prod] 2 | enabled = true 3 | mode = "custom" 4 | sshUser = "test" 5 | servers = ["127.0.0.1", "127.0.0.2", "127.0.0.2"] 6 | syncOrigin = 127.0.0.1 7 | useZFSreplication = false 8 | 9 | [localStorageConfig-dev] 10 | enabled = true 11 | mode = "diffuse" 12 | sshUser = "test" 13 | syncTime = 7200 14 | servers = ["127.0.0.1", "127.0.0.2", "127.0.0.2"] 15 | syncOrigin = 127.0.0.1 16 | useZFSreplication = false 17 | 18 | [localStorageConfig-test] 19 | enabled = true 20 | backupEnabled = True 21 | mode = "single" 22 | syncTime = 10 23 | storageUser = "test" 24 | customTarget = "127.0.0.1" 25 | syncTargets = "127.0.0.1 127.0.0.2 127.0.0.2" 26 | syncOrigin = 127.0.0.1 27 | targetFolder = ./static/uploads 28 | originFolder = ./static/uploads 29 | syncBackup = 127.0.0.1 30 | backupFolder = /tmp 31 | useZFSreplication = false 32 | -------------------------------------------------------------------------------- /integration_tests/guestEndpoints_test.go: -------------------------------------------------------------------------------- 1 | package integration_tests 2 | 3 | import ( 4 | "testing" 5 | 6 | _ "github.com/beego/beego/v2/server/web/session/redis" 7 | _ "github.com/dionyself/golang-cms/routers" 8 | 9 | log "github.com/beego/beego/v2/core/logs" 10 | . "github.com/smartystreets/goconvey/convey" 11 | ) 12 | 13 | // TestMain tests the main page 14 | func TestMain(t *testing.T) { 15 | jsonBody := []byte(`{"client_message": "hello, server!"}`) 16 | w := MakeRequest("GET", "/", map[string]string{"Content-Type": "application/json"}, jsonBody, nil) 17 | 18 | log.Trace("testing", "TestMain", "Code[%d]\n%s", w.Code, w.Body.String()) 19 | 20 | Convey("Subject: Test Station Endpoint\n", t, func() { 21 | Convey("Status Code Should Be 200", func() { 22 | So(w.Code, ShouldEqual, 200) 23 | }) 24 | Convey("The Result Should Not Be Empty", func() { 25 | So(w.Body.Len(), ShouldBeGreaterThan, 0) 26 | }) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /integration_tests/views: -------------------------------------------------------------------------------- 1 | ../views -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/beego/beego/v2/core/config" 5 | "github.com/beego/beego/v2/server/web" 6 | _ "github.com/beego/beego/v2/server/web/session/redis" 7 | _ "github.com/dionyself/golang-cms/core/template" 8 | _ "github.com/dionyself/golang-cms/routers" 9 | "github.com/dionyself/golang-cms/utils" 10 | _ "github.com/go-sql-driver/mysql" 11 | _ "github.com/lib/pq" 12 | _ "github.com/mattn/go-sqlite3" 13 | ) 14 | 15 | func init() { 16 | currentEnvironment, _ := config.String("RunMode") 17 | utils.SessionInit(currentEnvironment) 18 | } 19 | 20 | func main() { 21 | web.Run() 22 | } 23 | -------------------------------------------------------------------------------- /models/article.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Article model articles in db 8 | type Article struct { 9 | Id int `orm:"column(id);auto"` 10 | User *User `orm:"rel(fk)"` 11 | Title string `orm:"column(title);size(255);"` 12 | Content string `orm:"column(content);size(128)"` 13 | CreateTime time.Time `orm:"column(create_time);type(timestamp);auto_now_add"` 14 | Type int 15 | Stars int // we may need redis help with this 16 | AllowComments bool 17 | Category *Category `orm:"rel(fk);null;default(null)"` 18 | ArticleComment []*ArticleComment `orm:"reverse(many)"` 19 | Likes []*ArticleLike `orm:"reverse(many)"` 20 | } 21 | 22 | // ArticleComment model articles_comments in db 23 | type ArticleComment struct { 24 | Id int `orm:"column(id);auto"` 25 | User *User `orm:"rel(fk)"` 26 | Article *Article `orm:"rel(fk)"` 27 | Likes []*CommentLike `orm:"reverse(many)"` 28 | } 29 | 30 | // Category model categories in db 31 | type Category struct { 32 | Id int `orm:"column(id);auto"` 33 | Name string `orm:"column(name);size(128)"` 34 | Articles []*Article `orm:"reverse(many)"` 35 | } 36 | -------------------------------------------------------------------------------- /models/base.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "container/list" 5 | ) 6 | 7 | type EventType int 8 | 9 | const ( 10 | EVENT_JOIN = iota 11 | EVENT_LEAVE 12 | EVENT_MESSAGE 13 | ) 14 | 15 | type Event struct { 16 | Type EventType // JOIN, LEAVE, MESSAGE 17 | User string 18 | Timestamp int // Unix timestamp (secs) 19 | Content string 20 | } 21 | 22 | const archiveSize = 20 23 | 24 | // Event archives. 25 | var archive = list.New() 26 | 27 | // NewArchive saves new event to archive list. 28 | func NewArchive(event Event) { 29 | if archive.Len() >= archiveSize { 30 | archive.Remove(archive.Front()) 31 | } 32 | archive.PushBack(event) 33 | } 34 | 35 | // GetEvents returns all events after lastReceived. 36 | func GetEvents(lastReceived int) []Event { 37 | events := make([]Event, 0, archive.Len()) 38 | for event := archive.Front(); event != nil; event = event.Next() { 39 | e := event.Value.(Event) 40 | if e.Timestamp > int(lastReceived) { 41 | events = append(events, e) 42 | } 43 | } 44 | return events 45 | } 46 | -------------------------------------------------------------------------------- /models/blocks.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Block model blocks in db 8 | type Block struct { 9 | Id int `orm:"column(id);auto"` 10 | Name string `orm:"column(title);size(255);"` 11 | Content string `orm:"column(content);size(128)"` 12 | CreateTime time.Time `orm:"column(create_time);type(timestamp);auto_now_add"` 13 | Type string `orm:"column(type);size(128)"` 14 | IsActive bool 15 | Position int 16 | //Config []*BlockConfig `orm:"reverse(many)"` 17 | } 18 | 19 | // BlockConfig model 20 | type BlockConfig struct { 21 | Id int `orm:"column(id);auto"` 22 | Block *Block `orm:"rel(fk)"` 23 | Key string `orm:"column(title);size(255);"` 24 | Value string `orm:"column(title);size(255);"` 25 | Type string `orm:"column(title);size(255);"` 26 | CreateTime time.Time `orm:"column(create_time);type(timestamp);auto_now_add"` 27 | Config []*BlockConfig `orm:"reverse(many)"` 28 | } 29 | -------------------------------------------------------------------------------- /models/form.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // RegisterForm ... 4 | type RegisterForm struct { 5 | Name string `form:"name" valid:"Required;"` 6 | Email string `form:"email" valid:"Required;"` 7 | Username string `form:"username" valid:"Required;AlphaNumeric;MinSize(4);MaxSize(300)"` 8 | Password string `form:"password" valid:"Required;MinSize(4);MaxSize(30)"` 9 | PasswordRe string `form:"passwordre" valid:"Required;MinSize(4);MaxSize(30)"` 10 | Gender bool `form:"gender" valid:"Required"` 11 | InvalidFields map[string]string `form:"-"` 12 | } 13 | 14 | // ArticleForm ... 15 | type ArticleForm struct { 16 | Id int `form:"-"` 17 | Title string `form:"title" valid:"Required;MinSize(4);MaxSize(300)"` 18 | Category int `form:"category"` 19 | Content string `form:"content" valid:"Required;MinSize(50);MaxSize(2000)"` 20 | TopicTags string `form:"topic-tags" valid:"MinSize(4);MaxSize(300)"` 21 | TaggedUsers string `form:"tagged-users" valid:"MinSize(4);MaxSize(300)"` 22 | AllowReviews bool `form:"allow-reviews" valid:"Required"` 23 | AllowComments bool `form:"allow-comments" valid:"Required"` 24 | InvalidFields map[string]string `form:"-"` 25 | } 26 | 27 | // ImageForm ... 28 | type ImageForm struct { 29 | Name string `form:"name" valid:"Required;"` 30 | User int `form:"user" valid:"Required;Numeric"` 31 | Targets string `form:"target" valid:"Required;"` 32 | PivoteX int `form:"pivotex" valid:"Required;Numeric"` 33 | PivoteY int `form:"pivotey" valid:"Required;Numeric"` 34 | ImageType string `form:"name" valid:"Required;"` 35 | Description string `form:"description" valid:"Required;AlphaNumeric;MinSize(4);MaxSize(300)"` 36 | File []byte `form:"-"` 37 | InvalidFields map[string]string `form:"-"` 38 | } 39 | -------------------------------------------------------------------------------- /models/image.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Image model articles in db 4 | type Image struct { 5 | Id int `orm:"column(id);auto"` 6 | User *User `orm:"rel(fk)"` 7 | Title string `orm:"column(title);size(255);"` 8 | Url string `orm:"column(url);size(255);"` 9 | Type int 10 | Category *Category `orm:"rel(fk);null;default(null)"` 11 | } 12 | -------------------------------------------------------------------------------- /models/jsonresponses.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // UploadResultJSON model articles in db 4 | type UploadResultJSON struct { 5 | Id int `json:"id"` 6 | Msg string `json:"message"` 7 | Url string `orm:"column(url);size(255);"` 8 | } 9 | -------------------------------------------------------------------------------- /models/like.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // ArticleLike model 8 | type ArticleLike struct { 9 | Id int `orm:"column(id);auto"` 10 | User *User `orm:"rel(fk)"` 11 | CreateTime time.Time `orm:"column(create_time);type(timestamp);auto_now_add"` 12 | Article *Article `orm:"rel(fk)"` 13 | } 14 | 15 | // CommentLike model 16 | type CommentLike struct { 17 | Id int `orm:"column(id);auto"` 18 | User *User `orm:"rel(fk)"` 19 | CreateTime time.Time `orm:"column(create_time);type(timestamp);auto_now_add"` 20 | Comment *ArticleComment `orm:"rel(fk)"` 21 | } 22 | -------------------------------------------------------------------------------- /models/menu.go: -------------------------------------------------------------------------------- 1 | package models 2 | -------------------------------------------------------------------------------- /models/modules.go: -------------------------------------------------------------------------------- 1 | package models 2 | -------------------------------------------------------------------------------- /models/template.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Template model 8 | type Template struct { 9 | Id int `orm:"column(id);auto"` 10 | Name string `orm:"column(name);size(50);unique"` 11 | Style []*Style `orm:"reverse(many)"` 12 | Active bool `orm:"column(active)"` 13 | CreateTime time.Time `orm:"column(create_time);type(timestamp);auto_now_add"` 14 | } 15 | 16 | // Style model 17 | type Style struct { 18 | Id int `orm:"column(id);auto"` 19 | Name string `orm:"column(name);size(50);unique"` 20 | Template *Template `orm:"rel(fk)"` 21 | Active bool `orm:"column(active)"` 22 | CreateTime time.Time `orm:"column(create_time);type(timestamp);auto_now_add"` 23 | } 24 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/beego/beego/v2/client/orm" 7 | ) 8 | 9 | // User model 10 | type User struct { 11 | Id int `orm:"column(id);auto"` 12 | Username string `orm:"column(username);size(50);unique"` 13 | Email string `orm:"column(email);size(255);"` 14 | Password string `orm:"column(password);size(128)"` 15 | CreateTime time.Time `orm:"column(create_time);type(timestamp);auto_now_add"` 16 | Admin bool `orm:"column(admin)"` 17 | Rands string `orm:"size(10)"` 18 | Profile *Profile `orm:"rel(one)"` 19 | Article []*Article `orm:"reverse(many)"` 20 | Permissions string 21 | } 22 | 23 | // Profile model 24 | type Profile struct { 25 | Id int 26 | User *User `orm:"reverse(one)"` 27 | Name string 28 | Avatar string 29 | Age int16 30 | Lema string 31 | Description string 32 | Gender bool 33 | //Socials []*Social `orm:"reverse(many)"` 34 | } 35 | 36 | // GetPermissions get user permissions data 37 | func (*Profile) GetPermissions(user User) string { 38 | return user.Permissions 39 | } 40 | 41 | // registering all modules 42 | func init() { 43 | orm.RegisterModel( 44 | new(User), 45 | new(Profile), 46 | new(Article), 47 | new(ArticleComment), 48 | new(Category), 49 | new(ArticleLike), 50 | new(CommentLike), 51 | new(Template), 52 | new(Style), 53 | new(Image), 54 | new(Block), 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /models/validators.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/beego/beego/v2/core/logs" 5 | "github.com/beego/beego/v2/core/validation" 6 | "github.com/dionyself/golang-cms/utils" 7 | ) 8 | 9 | func init() { 10 | logs.SetLogger(logs.AdapterConsole, `{"level":1}`) 11 | } 12 | 13 | // Validate RegisterForm data 14 | func (form *RegisterForm) Validate() bool { 15 | validator := validation.Validation{} 16 | isValid := false 17 | var err error 18 | if isValid, err = validator.Valid(form); err != nil { 19 | logs.Error(err) 20 | } else { 21 | if isValid { 22 | if form.Password != form.PasswordRe { 23 | validator.SetError("PasswordRe", "Passwords did not match") 24 | isValid = false 25 | } 26 | } 27 | if !isValid { 28 | form.InvalidFields = make(map[string]string, len(validator.Errors)) 29 | for _, err := range validator.Errors { 30 | form.InvalidFields[err.Key] = err.Message 31 | logs.Debug(err.Key, err.Message) 32 | } 33 | } 34 | } 35 | return isValid 36 | } 37 | 38 | // Validate ArticleForm data 39 | func (form *ArticleForm) Validate() bool { 40 | validator := validation.Validation{} 41 | isValid := false 42 | var err error 43 | if isValid, err = validator.Valid(form); err != nil { 44 | logs.Error(err) 45 | } else { 46 | if isValid { 47 | if form.Category < 0 { 48 | validator.SetError("Category", "Invalid category") 49 | isValid = false 50 | } 51 | } 52 | if !isValid { 53 | form.InvalidFields = make(map[string]string, len(validator.Errors)) 54 | for _, err := range validator.Errors { 55 | form.InvalidFields[err.Key] = err.Message 56 | logs.Debug(err.Key, err.Message) 57 | } 58 | } 59 | } 60 | return isValid 61 | } 62 | 63 | // Validate ImageForm data 64 | func (form *ImageForm) Validate() bool { 65 | validator := validation.Validation{} 66 | isValid := false 67 | var err error 68 | if isValid, err = validator.Valid(form); err != nil { 69 | logs.Error(err) 70 | } else { 71 | if isValid { 72 | if !utils.ContainsKey(utils.ImageSizes, form.Targets) { 73 | validator.SetError("Category", "Invalid category") 74 | isValid = false 75 | } 76 | } 77 | if !isValid { 78 | form.InvalidFields = make(map[string]string, len(validator.Errors)) 79 | for _, err := range validator.Errors { 80 | form.InvalidFields[err.Key] = err.Message 81 | logs.Debug(err.Key, err.Message) 82 | } 83 | } 84 | } 85 | return isValid 86 | } 87 | -------------------------------------------------------------------------------- /routers/router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/beego/beego/v2/server/web" 5 | "github.com/dionyself/golang-cms/controllers" 6 | "github.com/dionyself/golang-cms/core/template" 7 | ) 8 | 9 | func init() { 10 | for template, styles := range template.Templates { 11 | for _, style := range styles { 12 | // web.BConfig.WebConfig.StaticDir 13 | web.SetStaticPath("/static/"+template+"/"+style, "views/"+template+"/styles/"+style) 14 | } 15 | } 16 | 17 | // guests request 18 | web.Router("/", &controllers.IndexController{}) 19 | web.Router("/login", &controllers.LoginController{}, "get:LoginView;post:Login") 20 | web.Router("/logout", &controllers.LoginController{}, "get:Logout") 21 | web.Router("/register", &controllers.LoginController{}, "get:RegisterView;post:Register") 22 | web.Router("/article/:id:int/:action:string", &controllers.ArticleController{}) 23 | 24 | // User requests 25 | web.Router("/ajax/image/:id:int", &controllers.AjaxController{}, "get:GetImageUploadStatus;post:PostImage") 26 | web.Router("/profile/:id:int/:action:string", &controllers.ProfileController{}, "get:UserPanelView") 27 | 28 | // filters 29 | web.InsertFilter("/profile/:id:int/show", web.BeforeRouter, controllers.AuthRequest) 30 | web.InsertFilter("/article/:id:int/edit", web.BeforeRouter, controllers.AuthRequest) 31 | web.InsertFilter("/article/:id:int/comment", web.BeforeRouter, controllers.AuthRequest) 32 | web.InsertFilter("/ajax/image/:id:int", web.BeforeRouter, controllers.AuthRequest) 33 | web.InsertFilter("/*", web.BeforeExec, controllers.DetectUserAgent) 34 | web.InsertFilter("/", web.BeforeExec, controllers.DetectUserAgent) 35 | } 36 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /app/golang-cms 4 | echo "Current version is $(git tag --sort=committerdate | tail -1)" 5 | bee run 6 | -------------------------------------------------------------------------------- /static/img/article_cms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/static/img/article_cms.png -------------------------------------------------------------------------------- /static/img/avatar/female-medium.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/static/img/avatar/female-medium.jpg -------------------------------------------------------------------------------- /static/img/avatar/female-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/static/img/avatar/female-small.jpg -------------------------------------------------------------------------------- /static/img/avatar/female-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/static/img/avatar/female-thumbnail.jpg -------------------------------------------------------------------------------- /static/img/avatar/male-medium.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/static/img/avatar/male-medium.jpg -------------------------------------------------------------------------------- /static/img/avatar/male-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/static/img/avatar/male-small.jpg -------------------------------------------------------------------------------- /static/img/avatar/male-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/static/img/avatar/male-thumbnail.jpg -------------------------------------------------------------------------------- /static/img/btttcc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/static/img/btttcc.png -------------------------------------------------------------------------------- /static/img/xmmr.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/static/img/xmmr.jpeg -------------------------------------------------------------------------------- /static/js/layout-desktop.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/static/js/layout-desktop.js -------------------------------------------------------------------------------- /static/js/layout-mobile.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/js/layout-tablet.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/static/js/layout-tablet.js -------------------------------------------------------------------------------- /static/js/layout-watch.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/static/js/layout-watch.js -------------------------------------------------------------------------------- /static/js/layout.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/static/js/layout.js -------------------------------------------------------------------------------- /static/js/main.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/static/js/main.js -------------------------------------------------------------------------------- /static/uploads/test.txt: -------------------------------------------------------------------------------- 1 | dummy 2 | -------------------------------------------------------------------------------- /utils/base.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "reflect" 8 | 9 | "github.com/beego/beego/v2/server/web" 10 | ) 11 | 12 | var CurrentEnvironment string 13 | var SuportedMimeTypes map[string][]string 14 | 15 | // Contains Verify if slice contains x string 16 | func Contains(stringSlice []string, stringToSearch string) bool { 17 | for _, stringElement := range stringSlice { 18 | if stringElement == stringToSearch { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | 25 | // ContainsKey Verify if map contains x string --DEPRECATED 26 | func ContainsKey(thisMap interface{}, key string) bool { 27 | keys := reflect.ValueOf(thisMap).MapKeys() 28 | for _, v := range keys { 29 | if v.Interface().(string) == key { 30 | return true 31 | } 32 | } 33 | return false 34 | } 35 | 36 | // Merge 2 maps 37 | func MergeMaps(map1 map[string]string, map2 map[string]string) map[string]string { 38 | for key, value := range map2 { 39 | map1[key] = value 40 | } 41 | return map1 42 | } 43 | 44 | // Detects mimetype of a file 45 | func DetectMimeType(file io.Reader) (string, error) { 46 | buff := make([]byte, 512) // docs tell that it take only first 512 bytes into consideration 47 | if _, err := file.Read(buff); err != nil { 48 | fmt.Println(err) // do something with that error 49 | return "", err 50 | } 51 | return http.DetectContentType(buff), nil 52 | } 53 | 54 | func init() { 55 | CurrentEnvironment, _ = web.AppConfig.String("RunMode") 56 | SuportedMimeTypes = make(map[string][]string) 57 | } 58 | -------------------------------------------------------------------------------- /utils/image.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "fmt" 7 | "image" 8 | "image/jpeg" 9 | "image/png" 10 | "io" 11 | "math/rand" 12 | "net" 13 | "os" 14 | "os/exec" 15 | "strings" 16 | "time" 17 | 18 | "github.com/beego/beego/v2/server/web" 19 | "github.com/dionyself/cutter" 20 | ) 21 | 22 | var ImageSizes map[string][2]int 23 | 24 | // Image cutter 25 | func CropImage(img io.Reader, imgMimeType string, target string, anchorCoord [2]int) (image.Image, error) { 26 | if MimeType, err := DetectMimeType(img); MimeType == imgMimeType && err == nil { 27 | var decodedImage, croppedImg image.Image 28 | switch MimeType { 29 | case "image/jpeg": 30 | decodedImage, err = jpeg.Decode(img) 31 | case "image/png": 32 | decodedImage, err = png.Decode(img) 33 | default: 34 | return nil, nil 35 | } 36 | if err == nil { 37 | croppedImg, _ = cutter.Crop( 38 | decodedImage, 39 | cutter.Config{ 40 | Width: ImageSizes[target][0], 41 | Height: ImageSizes[target][1], 42 | Anchor: image.Point{anchorCoord[0], anchorCoord[1]}, 43 | }, 44 | ) 45 | } 46 | return croppedImg, err 47 | } 48 | return nil, nil 49 | } 50 | 51 | // Image uploader 52 | func UploadImage(target string, img image.Image) error { 53 | localStorageBlk := "localStorageConfig-" + CurrentEnvironment + "::" 54 | if enabled, err := web.AppConfig.Bool(localStorageBlk + "enabled"); enabled && err == nil { 55 | name := getImageHash(img) 56 | url := fmt.Sprintf("./%s/%s_%v_%v.jpg", target, name, ImageSizes[target][0], ImageSizes[target][0]) 57 | out, err := os.Create(url) 58 | if err != nil { 59 | fmt.Println(err) 60 | os.Exit(1) 61 | } 62 | defer out.Close() 63 | if err = jpeg.Encode(out, img, nil); err == nil { 64 | return err 65 | } 66 | } 67 | return uploadToRemote() 68 | } 69 | 70 | // Image hashing 71 | func getImageHash(img image.Image) string { 72 | buf := new(bytes.Buffer) 73 | jpeg.Encode(buf, img, nil) 74 | imgBytes := buf.Bytes() 75 | hashBytes := md5.Sum(imgBytes) 76 | return fmt.Sprintf("%x", hashBytes) 77 | } 78 | 79 | func uploadToRemote() error { 80 | // error("TODO: image.uploadToRemote()") 81 | return nil 82 | } 83 | 84 | func syncronize(every time.Duration) { 85 | // var out bytes.Buffer 86 | var cmd *exec.Cmd 87 | localStorageBlk := "localStorageConfig-" + CurrentEnvironment + "::" 88 | backupEnabled, _ := web.AppConfig.Bool(localStorageBlk + "backupEnabled") 89 | 90 | source, _ := web.AppConfig.String(localStorageBlk + "originFolder") 91 | target := "" 92 | 93 | targetFolder, _ := web.AppConfig.String(localStorageBlk + "targetFolder") 94 | storageUser, _ := web.AppConfig.String(localStorageBlk + "storageUser") 95 | s_l, _ := web.AppConfig.String(localStorageBlk + "mode") 96 | servers := strings.Fields(s_l) 97 | customTarget, _ := web.AppConfig.String(localStorageBlk + "customTarget") 98 | syncBackup, _ := web.AppConfig.String(localStorageBlk + "syncBackup") 99 | backupFolder, _ := web.AppConfig.String(localStorageBlk + "backupFolder") 100 | 101 | for syncTime := range time.Tick(every) { 102 | target = "" 103 | fmt.Println("syncronzing files... at: ", syncTime) 104 | st_mode, _ := web.AppConfig.String(localStorageBlk + "mode") 105 | switch st_mode { 106 | case "single": 107 | if !backupEnabled { 108 | fmt.Println("Sync disabled on single mode...") 109 | return 110 | } 111 | case "diffuse": 112 | fmt.Println("syncronizing to local servers...") 113 | target = fmt.Sprintf("%s@%s:%s", storageUser, servers[rand.Intn(len(servers))], targetFolder) 114 | case "custom": 115 | fmt.Println("syncronizing to: ", customTarget) 116 | target = customTarget 117 | default: 118 | fmt.Println("sync FAILED an STOPPED at: ", syncTime) 119 | return 120 | } 121 | 122 | if target != "" { 123 | cmd = exec.Command("rsync", source, target) 124 | //cmd.Stdout = &out 125 | err := cmd.Run() 126 | if err != nil { 127 | fmt.Printf("FAILED %s", err) 128 | } 129 | } 130 | if backupEnabled { 131 | fmt.Println("syncronizing backups...") 132 | if !Contains(getLocalIPs(), syncBackup) { 133 | target = fmt.Sprintf("%s@%s:%s", storageUser, syncBackup, backupFolder) 134 | } else { 135 | target = backupFolder 136 | } 137 | cmd = exec.Command("rsync", source, target) 138 | cmd.Run() 139 | fmt.Println("files... syncronized at: ", syncTime) 140 | } 141 | } 142 | } 143 | 144 | func getLocalIPs() []string { 145 | var localIPs []string 146 | ifaces, _ := net.Interfaces() 147 | // handle err 148 | for _, i := range ifaces { 149 | addrs, _ := i.Addrs() 150 | // handle err 151 | for _, addr := range addrs { 152 | var ip net.IP 153 | switch v := addr.(type) { 154 | case *net.IPNet: 155 | ip = v.IP 156 | case *net.IPAddr: 157 | ip = v.IP 158 | } 159 | localIPs = append(localIPs, ip.String()) 160 | } 161 | } 162 | return localIPs 163 | } 164 | 165 | func init() { 166 | ImageSizes = make(map[string][2]int) 167 | ImageSizes["profile"] = [2]int{320, 160} 168 | ImageSizes["profile-min"] = [2]int{80, 70} 169 | ImageSizes["profile-icon"] = [2]int{30, 30} 170 | ImageSizes["cms"] = [2]int{3000, 2050} 171 | ImageSizes["cms-landing"] = [2]int{3000, 2050} 172 | ImageSizes["cms-content"] = [2]int{3000, 2050} 173 | ImageSizes["cms-wiget"] = [2]int{3000, 2050} 174 | ImageSizes["cms-min"] = [2]int{3000, 2050} 175 | ImageSizes["cms-icon"] = [2]int{3000, 2050} 176 | ImageSizes["custom-min"] = [2]int{30, 20} 177 | ImageSizes["custom-max"] = [2]int{3000, 2000} 178 | SuportedMimeTypes["images"] = []string{"image/png", "image/jpeg"} 179 | 180 | currentEnvironment, _ := web.AppConfig.String("RunMode") 181 | localStorageBlk := "localStorageConfig-" + currentEnvironment + "::" 182 | syncTime, _ := web.AppConfig.Int(localStorageBlk + "syncTime") 183 | syncEnabled, _ := web.AppConfig.Bool(localStorageBlk + "enabled") 184 | if syncTime != 0 && syncEnabled { 185 | go syncronize(time.Duration(syncTime) * time.Second) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /utils/passwd.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "encoding/hex" 8 | "hash" 9 | ) 10 | 11 | // GetRandomString ... 12 | func GetRandomString(amount int) (randomString string) { 13 | const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 14 | var bytes = make([]byte, amount) 15 | rand.Read(bytes) 16 | for index, _byte := range bytes { 17 | bytes[index] = alphanum[_byte%byte(len(alphanum))] 18 | } 19 | return string(bytes) 20 | } 21 | 22 | // EncodePassword ... 23 | func EncodePassword(rawPassword string, salt string) (encodedPassword string) { 24 | password := PBKDF2([]byte(rawPassword), []byte(salt), 10000, 50, sha256.New) 25 | return hex.EncodeToString(password) 26 | } 27 | 28 | // PBKDF2 http://code.google.com/p/go/source/browse/pbkdf2/pbkdf2.go?repo=crypto 29 | func PBKDF2(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte { 30 | prf := hmac.New(h, password) 31 | hashLen := prf.Size() 32 | numBlocks := (keyLen + hashLen - 1) / hashLen 33 | 34 | var buf [4]byte 35 | dk := make([]byte, 0, numBlocks*hashLen) 36 | U := make([]byte, hashLen) 37 | for block := 1; block <= numBlocks; block++ { 38 | // N.B.: || means concatenation, ^ means XOR 39 | // for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter 40 | // U_1 = PRF(password, salt || uint(i)) 41 | prf.Reset() 42 | prf.Write(salt) 43 | buf[0] = byte(block >> 24) 44 | buf[1] = byte(block >> 16) 45 | buf[2] = byte(block >> 8) 46 | buf[3] = byte(block) 47 | prf.Write(buf[:4]) 48 | dk = prf.Sum(dk) 49 | T := dk[len(dk)-hashLen:] 50 | copy(U, T) 51 | 52 | // U_n = PRF(password, U_(n-1)) 53 | for n := 2; n <= iter; n++ { 54 | prf.Reset() 55 | prf.Write(U) 56 | U = U[:0] 57 | U = prf.Sum(U) 58 | for x := range U { 59 | T[x] ^= U[x] 60 | } 61 | } 62 | } 63 | return dk[:keyLen] 64 | } 65 | -------------------------------------------------------------------------------- /utils/session.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/beego/beego/v2/server/web" 5 | ) 6 | 7 | // SessionInit ... 8 | func SessionInit(env string) { 9 | sessBlk := "sessionConfig-" + env + "::" 10 | provider, _ := web.AppConfig.String("SessionProvider") 11 | Address := "" 12 | if provider == "redis" { 13 | Address, _ = web.AppConfig.String(sessBlk + "redisServer") 14 | if Address == "" { 15 | Address, _ = web.AppConfig.String("cacheConfig-" + env + "::redisMasterServer") 16 | } 17 | } 18 | 19 | web.BConfig.WebConfig.Session.SessionName, _ = web.AppConfig.String(sessBlk + "cookieName") 20 | web.BConfig.WebConfig.Session.SessionGCMaxLifetime, _ = web.AppConfig.Int64(sessBlk + "gclifetime") 21 | web.BConfig.WebConfig.Session.SessionProviderConfig = Address 22 | web.BConfig.Listen.EnableHTTPS, _ = web.AppConfig.Bool(sessBlk + "secure") 23 | web.BConfig.WebConfig.Session.SessionAutoSetCookie, _ = web.AppConfig.Bool(sessBlk + "enableSetCookie") 24 | web.BConfig.WebConfig.Session.SessionDomain, _ = web.AppConfig.String(sessBlk + "domain") 25 | web.BConfig.WebConfig.Session.SessionCookieLifeTime, _ = web.AppConfig.Int(sessBlk + "cookieLifeTime") 26 | } 27 | -------------------------------------------------------------------------------- /views/default/article-editor.html: -------------------------------------------------------------------------------- 1 | {{.form.InvalidFields}} 2 |
3 | 4 | 5 |
6 | 7 | 13 |
14 | mode_edit 15 | 16 | 19 | 24 | 25 | 26 |
27 | Comments: 28 | 34 | Reviews: 35 | 41 |
42 | 43 | 44 | 45 |
46 | -------------------------------------------------------------------------------- /views/default/article.html: -------------------------------------------------------------------------------- 1 |
2 |
{{.Article.Title}}
3 | {{str2html .Article.Content}} 4 |
5 | 24 |
25 | :gnitaR
26 |
27 |
28 | 29 |
30 | 31 |
32 | -------------------------------------------------------------------------------- /views/default/blocks/html_block.html: -------------------------------------------------------------------------------- 1 |
2 | This is a test of the concept of blocks 3 |
{{.SectionData.body}}
4 |
5 | -------------------------------------------------------------------------------- /views/default/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Welcome to Golang-CMS!

4 |

5 | Golang-CMS is a full & powerful Go web content management system 6 | which is inspired by djando-cms. 7 |
8 | Official website: {{.Website}} 9 |
10 | Contact me: {{.Email}} 11 |
you are visiting the page from a {{.device_type}} device 12 |
To get more info please: Register OR login 14 |

15 |
16 |
17 | -------------------------------------------------------------------------------- /views/default/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | your account 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{.Head}} 15 | 16 | 17 | 18 | {{.Styles}} 19 | 20 | 21 | 22 | 23 | 24 | 62 |
63 | 64 |
65 | {{.LayoutContent}} 66 |
67 | {{.Block_1}}
{{.Block_2}} 68 | 114 | 115 | {{.Scripts}} 116 | 117 | 118 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /views/default/login.html: -------------------------------------------------------------------------------- 1 |

Login

2 |
3 | 4 | 5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /views/default/partial/html_head_desktop.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/views/default/partial/html_head_desktop.html -------------------------------------------------------------------------------- /views/default/partial/html_head_mobile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /views/default/partial/html_head_tablet.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/views/default/partial/html_head_tablet.html -------------------------------------------------------------------------------- /views/default/partial/html_head_watch.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/views/default/partial/html_head_watch.html -------------------------------------------------------------------------------- /views/default/profile-view.html: -------------------------------------------------------------------------------- 1 |

Welcome to you account

2 |

your username is " {{.user.Username}}" you user id is: " {{.user.Id}}

3 | -------------------------------------------------------------------------------- /views/default/register.html: -------------------------------------------------------------------------------- 1 |

Register

2 |
3 |
4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 15 |
16 |
17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 |
39 |
40 | -------------------------------------------------------------------------------- /views/default/styles/default/img/avatar/female-medium.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/views/default/styles/default/img/avatar/female-medium.jpg -------------------------------------------------------------------------------- /views/default/styles/default/img/avatar/female-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/views/default/styles/default/img/avatar/female-small.jpg -------------------------------------------------------------------------------- /views/default/styles/default/img/avatar/female-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/views/default/styles/default/img/avatar/female-thumbnail.jpg -------------------------------------------------------------------------------- /views/default/styles/default/img/avatar/male-medium.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/views/default/styles/default/img/avatar/male-medium.jpg -------------------------------------------------------------------------------- /views/default/styles/default/img/avatar/male-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/views/default/styles/default/img/avatar/male-small.jpg -------------------------------------------------------------------------------- /views/default/styles/default/img/avatar/male-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/views/default/styles/default/img/avatar/male-thumbnail.jpg -------------------------------------------------------------------------------- /views/default/styles/default/js/layout-desktop.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/views/default/styles/default/js/layout-desktop.js -------------------------------------------------------------------------------- /views/default/styles/default/js/layout-mobile.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /views/default/styles/default/js/layout-tablet.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/views/default/styles/default/js/layout-tablet.js -------------------------------------------------------------------------------- /views/default/styles/default/js/layout-watch.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/views/default/styles/default/js/layout-watch.js -------------------------------------------------------------------------------- /views/default/styles/default/js/layout.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/views/default/styles/default/js/layout.js -------------------------------------------------------------------------------- /views/default/styles/default/js/main.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dionyself/golang-cms/be9d54ad824f00689cbed07eb81500b693375c66/views/default/styles/default/js/main.js -------------------------------------------------------------------------------- /views/default/user-profile.html: -------------------------------------------------------------------------------- 1 |

Welcome to your account: {{.user.Profile.Name}}

2 |

your username is " {{.user.Username}}" you user id is: " {{.user.Id}} "

3 |
4 |
5 |
6 |
7 |
8 | 9 | {{.user.Profile.Name}} 10 |
11 |
12 |

{{.user.Profile.Description}}

13 |
14 |
15 | This is a link 16 |
17 |
18 |
19 |
20 |
    21 |
  • 22 | Publish New Article 23 |
  • 24 |
  • 25 | Friends 26 |
  • 27 |
  • 28 | Apps 29 |
  • 30 |
31 |
32 |
33 | Memu 2 34 |
35 |
36 |
37 | --------------------------------------------------------------------------------