├── .gitignore ├── .htaccess ├── CREDITS.txt ├── INSTALL.md ├── LICENSE.txt ├── README.md ├── TODO.txt ├── cron.cgi ├── examples ├── Dockerfile ├── robots.txt ├── settings.lua └── vault.lua ├── includes ├── bootstrap.lua ├── common.lua ├── database │ ├── init.lua │ ├── postgresql.lua │ └── sqlite3.lua ├── debug.lua ├── form.lua ├── locale.lua ├── mobile.lua ├── module.lua ├── pager.lua ├── route.lua ├── server │ ├── cgi.lua │ ├── init.lua │ └── nginx.lua ├── session.lua └── theme.lua ├── index.cgi ├── install.cgi ├── libraries ├── jquery.js ├── jquery.min.js ├── jquery.min.map ├── json2.js ├── ophal.js └── uuid.js ├── lighttpd.ophal.lua ├── modules ├── boost │ └── init.lua ├── comment │ ├── comment.js │ ├── comment.tpl.html │ └── init.lua ├── content │ ├── content.js │ ├── content_page.tpl.html │ ├── content_teaser.tpl.html │ └── init.lua ├── file │ ├── file.js │ └── init.lua ├── lorem_ipsum │ └── init.lua ├── menu │ └── init.lua ├── system │ └── init.lua ├── tag │ ├── init.lua │ └── tag_form.js ├── test │ └── init.lua └── user │ ├── admin.lua │ ├── init.lua │ └── user_login.js ├── nginx.ophal.conf └── themes ├── basic ├── footer.tpl.html ├── html.tpl.html ├── images │ ├── ophal.ico │ └── ophalproject.png ├── sidebar_first.tpl.html └── style.css ├── install ├── footer.tpl.html ├── html.tpl.html ├── images │ ├── ophal.ico │ └── ophalproject.png ├── sidebar_first.tpl.html └── style.css └── mobile ├── footer.tpl.html ├── html.tpl.html ├── images ├── ophal.ico └── ophalproject.png └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | /files/* 2 | /vault.lua 3 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | Options +ExecCGI 2 | 3 | # Allow execution of .cgi scripts. 4 | AddHandler cgi-script .cgi 5 | 6 | # Protect files and directories from prying eyes. 7 | 8 | Order allow,deny 9 | 10 | 11 | # Handle any 404 errors. 12 | ErrorDocument 404 /index.cgi 13 | 14 | # Set the default handler. 15 | DirectoryIndex index.cgi 16 | 17 | # Force simple error message for requests for non-existent favicon.ico. 18 | 19 | # There is no end quote below, for compatibility with Apache 1.3. 20 | ErrorDocument 404 "The requested file favicon.ico was not found. 21 | 22 | 23 | # Etags rules. 24 | 25 | # Disabled: 26 | # FileETag None 27 | 28 | # Enable Etags: 29 | # FileETag MTime Size 30 | 31 | # Enable Etags in a clustered environment: 32 | # FileETag MTime Size 33 | 34 | 35 | # Various rewrite rules. 36 | 37 | RewriteEngine on 38 | 39 | # Do not allow access to "hidden" directories whose names begin with period. 40 | # NOTE: This only works when mod_rewrite is loaded. 41 | RewriteRule "(^|/)\." - [F] 42 | 43 | # Rewrite URLs of the form 'x' to the form 'index.cgi?q=x'. 44 | RewriteCond %{REQUEST_FILENAME} !-f 45 | RewriteCond %{REQUEST_FILENAME} !-d 46 | RewriteCond %{REQUEST_URI} !=/favicon.ico 47 | # Don't care about image files 48 | RewriteCond %{REQUEST_URI} !\.(jpg|JPG|gif|GIF|png|PNG|ico|ICO|svg|SVG)$ 49 | RewriteRule ^(.*)$ index.cgi [L] 50 | 51 | -------------------------------------------------------------------------------- /CREDITS.txt: -------------------------------------------------------------------------------- 1 | Ophal - Highly scalable web platform written in Lua 2 | Copyright (C) 2011 - 2015 Fernando Paredes García 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as 6 | published by the Free Software Foundation, either version 3 of the 7 | License, or (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with this program. If not, see . 16 | 17 | 18 | Some portions of source code have been copied or adapted from following 19 | software: 20 | 21 | Drupal 22 | ------ 23 | All Drupal code is Copyright 2001 - 2010 by the original authors. 24 | Ophal is inspired on Drupal API. 25 | 26 | Nutria Nemo 27 | ----------- 28 | All Nutria code is Copyright 2010 - 2011 Fernando Paredes Garcia 29 | Ophal reuses portions of Nutria Nemo's source code. 30 | 31 | 32 | Other software compatible with GPL license is included: 33 | 34 | jQuery 35 | ------ 36 | Copyright (c) 2008 - 2011 John Resig 37 | 38 | Mobile_Detect 39 | ------------- 40 | Copyright (c) 2009 - 2012 Serban Ghita 41 | http://code.google.com/p/php-mobile-detect/ - MIT License 42 | 43 | json2.js 44 | -------- 45 | Public Domain. 46 | 47 | uuid.js 48 | ------- 49 | Copyright (C) 2011 Alexey Silin 50 | https://gist.github.com/LeverOne/1308368 51 | 52 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Fernando Paredes García, https://www.develcuy.com/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ophal - An experimental Lua based CMS/CMF 2 | 3 | ## What is Ophal? 4 | 5 | Ophal aimed to become a highly scalable web platform, easy to maintain, learn, extend and open to improvements. 6 | 7 | ## Dependencies 8 | 9 | Ophal has the following dependencies: 10 | 11 | - Seawolf (http://github.com/ophal/seawolf) 12 | - LuaSocket 13 | - LPEG 14 | - LuaFilesystem 15 | - LuaDBI 16 | - luuid 17 | - dkjson 18 | - LuaCrypto (only if user module is enabled) 19 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | - Beta release: Write documentation 2 | - Move this TODO list to a case tracker or similar 3 | - Move README.txt to on-line documentation 4 | - Use hook:bootstrap to allow modules call its dependencies when needed 5 | - Use hook:bootstrap to fill the list of Ophal modules to load 6 | - Use hook:environment to fill the list of Lua functions to load, modules call its dependencies when needed 7 | - Optionally (setting based), build a cache of dependencies based on path 8 | - Implement headers handler 9 | - Document API with Doxygen or similar 10 | - Implement lazy loading API: load things only when they are called 11 | - HTML5 by default (Websockets and all the fireworks yay!) 12 | - Flush output buffer on errors 13 | - Include HTTP headers parser written by daurnimator https://gist.github.com/3480374 14 | - Rename hook_menu to hook_path, menu.inc to path.inc 15 | - Make clear separation between Path core component and Menu core module 16 | - Handling of Menus and Paths should be best easy 17 | - Use lua states instead of empty ENV tables: https://github.com/mbalmer/luaproxy 18 | - User module: logins and access log 19 | -------------------------------------------------------------------------------- /cron.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua5.1 2 | 3 | require 'includes.bootstrap' 4 | 5 | settings.output_buffering = false 6 | 7 | ophal.bootstrap(nil, function () 8 | if not settings.maintenance_mode then 9 | module_invoke_all 'cron' 10 | end 11 | 12 | -- Output something to prevent error 500 13 | print '' 14 | end) 15 | -------------------------------------------------------------------------------- /examples/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Dockerfile for Ophal 3 | # 4 | # version: 0.2.0 5 | # 6 | 7 | FROM debian:jessie 8 | MAINTAINER Fernando Paredes Garcia 9 | 10 | # Update packages 11 | RUN apt-get update 12 | RUN apt-get dist-upgrade -y 13 | 14 | # Install package dependencies 15 | RUN apt-get install -y supervisor vim make less git curl lua5.1 libpcre3-dev sqlite3 libsqlite3-dev libssl-dev uuid-dev 16 | 17 | # Install luarocks 18 | RUN apt-get install -y luarocks 19 | 20 | # Install Ophal 21 | RUN luarocks install lrexlib-pcre PCRE_LIBDIR=/usr/lib/x86_64-linux-gnu/ 22 | RUN luarocks install luadbi-sqlite3 SQLITE_INCDIR=/usr/include/ 23 | RUN luarocks install lpeg 24 | RUN luarocks install bit32 25 | RUN luarocks install md5 26 | RUN luarocks install luasec OPENSSL_LIBDIR=/usr/lib/x86_64-linux-gnu/ 27 | RUN luarocks install dkjson 28 | RUN luarocks install --server=http://rocks.moonscript.org/dev seawolf 1.0-0 29 | RUN luarocks install --server=http://rocks.moonscript.org/dev ophal-cli 30 | 31 | # Install Apache 32 | RUN apt-get install -y apache2-mpm-worker 33 | 34 | # Configure Apache 35 | RUN echo '[supervisord]\n\ 36 | nodaemon=true\n\ 37 | \n\ 38 | [program:apache2]\n\ 39 | command=/usr/bin/pidproxy /var/run/apache2/apache2.pid /bin/bash -c "source /etc/apache2/envvars && /usr/sbin/apache2 -DFOREGROUND"\n\ 40 | redirect_stderr=true\n'\ 41 | >> /etc/supervisor/conf.d/supervisord.conf 42 | RUN mkdir /var/run/apache2 /var/lock/apache2 && chown www-data: /var/lock/apache2 /var/run/apache2 43 | RUN echo '\n\ 44 | \n\ 45 | ServerAdmin webmaster@localhost\n\ 46 | \n\ 47 | DocumentRoot /var/www\n\ 48 | \n\ 49 | Options FollowSymLinks\n\ 50 | AllowOverride None\n\ 51 | \n\ 52 | \n\ 53 | Options Indexes FollowSymLinks MultiViews\n\ 54 | AllowOverride All\n\ 55 | Order allow,deny\n\ 56 | allow from all\n\ 57 | \n\ 58 | \n\ 59 | ErrorLog ${APACHE_LOG_DIR}/error.log\n\ 60 | CustomLog ${APACHE_LOG_DIR}/access.log combined\n\ 61 | \n\ 62 | '\ 63 | > /etc/apache2/sites-available/000-default.conf 64 | RUN a2enmod rewrite 65 | RUN a2enmod cgid 66 | RUN service apache2 restart 67 | VOLUME ["/var/www/"] 68 | EXPOSE 80 443 69 | 70 | # Create deploy user 71 | RUN useradd deploy 72 | 73 | # Start supervisor 74 | CMD ["/usr/bin/supervisord"] 75 | -------------------------------------------------------------------------------- /examples/robots.txt: -------------------------------------------------------------------------------- 1 | # 2 | # robots.txt 3 | # 4 | # This file is to prevent the crawling and indexing of certain parts 5 | # of your site by web crawlers and spiders run by sites like Yahoo! 6 | # and Google. By telling these "robots" where not to go on your site, 7 | # you save bandwidth and server resources. 8 | # 9 | # This file will be ignored unless it is at the root of your host: 10 | # Used: http://example.com/robots.txt 11 | # Ignored: http://example.com/site/robots.txt 12 | # 13 | # How to use it? Rename this file to "robots.txt". 14 | # 15 | # For more information about the robots.txt standard, see: 16 | # http://www.robotstxt.org/robotstxt.html 17 | # 18 | # For syntax checking, see: 19 | # https://webmaster.yandex.com/robots.xml 20 | 21 | User-agent: * 22 | Crawl-delay: 10 23 | # Directories 24 | Disallow: /includes/ 25 | Disallow: /misc/ 26 | Disallow: /modules/ 27 | Disallow: /themes/ 28 | # Files 29 | Disallow: /cron.cgi 30 | Disallow: /install.cgi 31 | Disallow: /CREDITS.txt 32 | Disallow: /INSTALL.md 33 | Disallow: /LICENSE.txt 34 | Disallow: /README.txt 35 | # Paths (clean URLs) 36 | Disallow: /admin/ 37 | Disallow: /user/register/ 38 | Disallow: /user/password/ 39 | Disallow: /user/login/ 40 | Disallow: /user/logout/ 41 | -------------------------------------------------------------------------------- /examples/settings.lua: -------------------------------------------------------------------------------- 1 | return function(settings, vault) 2 | settings.language = 'en' 3 | settings.language_dir = 'ltr' 4 | settings.site = { 5 | -- scheme = 'http', 6 | -- domain_name = 'www.example.com', 7 | frontpage = 'lorem_ipsum', 8 | name = 'Ophal', 9 | hash = vault.site.hash, 10 | logo_title = 'The Ophal Project', 11 | logo_path = 'images/ophalproject.png', 12 | files_path = 'files', 13 | } 14 | settings.micro_cache = false 15 | settings.debugapi = true 16 | settings.maintenance_mode = false 17 | settings.output_buffering = false 18 | settings.sessionapi = { 19 | enabled = true, 20 | ttl = 86400, 21 | lock_ttl = 120, 22 | } 23 | settings.formapi = false 24 | settings.date_format = '!%Y-%m-%d %H:%M UTC' 25 | settings.route_aliases_storage = false 26 | settings.route_aliases_prepend_language = false 27 | settings.route_redirects_storage = false 28 | settings.route_redirects_prepend_language = false 29 | 30 | --[[ Active/Disabled modules 31 | List of Ophal modules to load on bootstrap. 32 | 33 | Example: 34 | 35 | settings.modules = { 36 | mymodule = true, 37 | othermodule = false, -- disabled module 38 | } 39 | ]] 40 | settings.modules = { 41 | lorem_ipsum = true, 42 | } 43 | 44 | --[[ Database connection settings (see vault.lua) ]] 45 | settings.db = vault.db 46 | 47 | --[[ Extend jailed environment 48 | Ophal code is jailed into an environment with few functions. Use the 49 | global variable 'env' to add external functions and lua modules. 50 | 51 | Example: 52 | 53 | require 'external.library' 54 | env.myfunction = external.library.function 55 | ]] 56 | 57 | --[[ 58 | Theme settings. 59 | ]] 60 | settings.theme = { 61 | name = 'basic', 62 | } 63 | 64 | --[[ Extend templates environment 65 | Template files (like: *.tpl.*) i.e: page.tpl.html, have a very limited 66 | set of functions available. Use setting 'template_env' to add external 67 | functions and lua modules. 68 | NOTE: Template variables are not overridden by the ones with this setting. 69 | 70 | Example: 71 | 72 | settings.template_env = {} 73 | 74 | require 'external.library' 75 | settings.template_env.myfunction = external.library.function 76 | ]] 77 | 78 | --[[ Mobile support settings 79 | The mobile_detect library is a helper for mobile web development. 80 | Set settings.mobile to nil to turn off mobile support. 81 | Always make sure to set settings.domain_name if settings.redirect is 82 | set to true. 83 | 84 | Example: 85 | settings.mobile = { 86 | theme = 'mobile', 87 | domain_name = 'mobile.mydomain.com', 88 | redirect = true, 89 | } 90 | ]] 91 | 92 | --[[ 93 | Boost provides static cache by saving all the output to files. 94 | 95 | Example: 96 | 97 | settings.modules.boost = true 98 | settings.boost = { 99 | path = 'files/boost/', 100 | lifetime = 3600, -- seconds 101 | signature = '', 102 | date_format = '!%Y-%m-%d %T UTC', 103 | } 104 | ]] 105 | end 106 | -------------------------------------------------------------------------------- /examples/vault.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This file is for storage of sensitive information ONLY. 3 | ]] 4 | 5 | local m = { 6 | -- Following example tries to keep the structure from settings.lua 7 | 8 | --[[ Site settings ]] 9 | site = { 10 | hash = nil, 11 | }, 12 | 13 | --[[ Database connection settings 14 | Ophal automatically connects on bootstrap to a database if a the key 15 | 'db' is set with connection settings. 16 | 17 | Example: 18 | 19 | settings.db = { 20 | default = { 21 | driver = 'PostgreSQL', 22 | database = 'database', 23 | username = 'username', 24 | password = 'password', 25 | host = 'localhost', 26 | port = '5432', 27 | } 28 | } 29 | ]] 30 | db = { 31 | default = { 32 | driver = 'SQLite3', 33 | database = '/path/to/database.ext', 34 | } 35 | }, 36 | } 37 | 38 | return m 39 | -------------------------------------------------------------------------------- /includes/bootstrap.lua: -------------------------------------------------------------------------------- 1 | local version = { 2 | core = 'Ophal', 3 | number = '0.2', 4 | -- revision = 'dev', 5 | homepage = 'ophal.org', 6 | } 7 | 8 | -- Jailed environment functions and modules 9 | env = { 10 | io = io, 11 | os = os, 12 | tonumber = tonumber, 13 | type = type, 14 | module = module, 15 | pcall = pcall, 16 | nopcall = function(f, ...) return true, f(...) end, 17 | loadstring = loadstring, 18 | setfenv = setfenv, 19 | getfenv = getfenv, 20 | assert = assert, 21 | table = table, 22 | require = require, 23 | unpack = unpack, 24 | pairs = pairs, 25 | ipairs = ipairs, 26 | rawset = rawset, 27 | rawget = rawget, 28 | error = error, 29 | debug = debug, 30 | package = package, 31 | string = string, 32 | math = math, 33 | next = next, 34 | tostring = tostring, 35 | setmetatable = setmetatable, 36 | getmetatable = getmetatable, 37 | select = select, 38 | _SERVER = os.getenv, 39 | _SESSION = nil, 40 | _VERSION = _VERSION, 41 | lfs = nil, 42 | lpeg = nil, 43 | uuid = nil, 44 | socket = nil, 45 | theme = {}, 46 | mobile = {}, 47 | base = { 48 | system_root = '', 49 | route = '/', 50 | url = '', 51 | path = '', 52 | }, 53 | output_buffer = {}, 54 | ophal = { 55 | version = nil, 56 | modules = {}, 57 | routes = {}, 58 | aliases = {}, 59 | redirects = {}, 60 | blocks = {}, 61 | regions = {}, 62 | title = '', 63 | header_title = '', 64 | cookies = {}, 65 | header = nil, 66 | session = nil, 67 | }, 68 | } 69 | 70 | -- Build settings 71 | settings = (function() 72 | local settings = { 73 | version = { 74 | core = true, 75 | number = true, 76 | revision = true, 77 | }, 78 | slash = string.sub(package.config,1,1), 79 | modules = {}, 80 | debugapi = true, 81 | } 82 | 83 | -- Load settings.lua and vault.lua 84 | local _, vault = pcall(require, 'vault') 85 | local _, settings_builder = pcall(require, 'settings') 86 | if type(settings_builder) == 'function' then 87 | settings_builder(settings, vault) 88 | end 89 | 90 | return settings 91 | end)() 92 | 93 | env.settings = settings 94 | 95 | -- Build version 96 | if settings.version.core then 97 | if settings.version.number then 98 | if settings.version.revision then 99 | env.ophal.version = ('%s %s-%s (%s)'):format(version.core, version.number, version.revision, version.homepage) 100 | else 101 | env.ophal.version = ('%s %s (%s)'):format(version.core, version.number, version.homepage) 102 | end 103 | else 104 | env.ophal.version = ('%s (%s)'):format(version.core, version.homepage) 105 | end 106 | end 107 | 108 | -- Detect nginx 109 | if ngx then 110 | env.ngx = ngx 111 | env.pcall = env.nopcall 112 | for k, v in pairs(getfenv(0, ngx)) do 113 | env[k] = v 114 | end 115 | end 116 | 117 | -- The actual module 118 | local setfenv, type, env = setfenv, type, env 119 | module 'ophal' 120 | 121 | function bootstrap(phase, main) 122 | if type(main) ~= 'function' then main = function() end end 123 | 124 | local status, err, exit_bootstrap 125 | 126 | -- Jail 127 | setfenv(0, env) -- global environment 128 | setfenv(1, env) -- bootstrap environment 129 | setfenv(main, env) -- script environment 130 | env._G = env 131 | env.env = env 132 | 133 | local phases = { 134 | -- 1. Lua and Seawolf libraries 135 | function () 136 | env.lfs = require 'lfs' 137 | env.lpeg = require 'lpeg' 138 | env.uuid = require 'uuid' 139 | 140 | env.socket = require 'socket' 141 | env.socket.url = require 'socket.url' 142 | 143 | env.seawolf = require 'seawolf'.__build('variable', 'fs', 'text', 'behaviour', 'contrib') 144 | end, 145 | 146 | -- 2. Debug API 147 | function () 148 | if settings.debugapi then 149 | require 'includes.debug' 150 | end 151 | end, 152 | 153 | -- 3. Load native server API 154 | function () 155 | if ngx then 156 | require 'includes.server.nginx' 157 | else 158 | require 'includes.server.cgi' 159 | end 160 | end, 161 | 162 | -- 4. Mobile API, 163 | function () 164 | if settings.mobile then 165 | require 'includes.mobile' 166 | end 167 | end, 168 | 169 | -- 5. Load Ophal server API 170 | function () 171 | require 'includes.server.init' 172 | build_base() 173 | end, 174 | 175 | -- 6. Check installer 176 | function () 177 | if (_SERVER 'SCRIPT_NAME' or '/index.cgi') == base.route .. 'index.cgi' and not seawolf.fs.is_file 'settings.lua' then 178 | require 'includes.common' 179 | redirect(('%s%sinstall.cgi'):format(base.system_root, base.route)) 180 | return -1 181 | end 182 | end, 183 | 184 | -- 7. Session API, 185 | function () 186 | local empty = seawolf.variable.empty 187 | if not empty(settings.sessionapi) then 188 | require 'includes.session' 189 | session_start() 190 | end 191 | end, 192 | 193 | -- 8. Route API, 194 | function () 195 | require 'includes.route' 196 | end, 197 | 198 | -- 9. Core API, 199 | function () 200 | require 'includes.common' 201 | require 'includes.locale' 202 | require 'includes.module' 203 | require 'includes.theme' 204 | require 'includes.pager' 205 | if settings.formapi then 206 | require 'includes.form' 207 | end 208 | end, 209 | 210 | -- 10. Modules, 211 | function () 212 | module_load_all() 213 | end, 214 | 215 | -- 11. Boot, 216 | function () 217 | module_invoke_all 'boot' 218 | end, 219 | 220 | -- 12. Database API, 221 | function () 222 | if settings.db ~= nil then 223 | require 'includes.database.init' 224 | if settings.db.default ~= nil then 225 | db_connect() 226 | if settings.route_aliases_storage then 227 | route_aliases_load() 228 | end 229 | if settings.route_redirects_storage then 230 | route_redirects_load() 231 | end 232 | end 233 | end 234 | end, 235 | 236 | -- 13. Init, 237 | function () 238 | route_redirect() 239 | module_invoke_all 'init' 240 | end, 241 | 242 | -- 14. Full, 243 | function () 244 | -- call hook route to load handlers 245 | -- TODO: implement route cache 246 | ophal.routes = route_build_routes() 247 | 248 | theme_blocks_load() 249 | theme_regions_load() 250 | 251 | -- process current route 252 | init_route() 253 | end, 254 | } 255 | 256 | -- Loop over phase 257 | for p = 1, (phase or #phases) do 258 | status, err = pcall(phases[p]) 259 | if not status then 260 | io.write(([[ 261 | 262 | bootstrap[%s]: %s]]):format(p, err or '')) 263 | exit_bootstrap = true 264 | break 265 | elseif err == -1 then 266 | exit_bootstrap = true 267 | break 268 | end 269 | end 270 | 271 | -- execute script 272 | if not exit_bootstrap then 273 | status, err = pcall(main) 274 | if not status then 275 | io.write([[ 276 | 277 | bootstrap[main]: ]] .. (err or '')) 278 | end 279 | end 280 | 281 | -- The end 282 | exit_ophal() 283 | end 284 | -------------------------------------------------------------------------------- /includes/common.lua: -------------------------------------------------------------------------------- 1 | local seawolf = require 'seawolf'.__build('maths', 'text', 'fs') 2 | local pairs, tcon, rawset, date = pairs, table.concat, rawset, os.date 3 | local base, lfs, json, round = base, lfs, require 'dkjson', seawolf.maths.round 4 | local str_replace, is_file = seawolf.text.str_replace, seawolf.fs.is_file 5 | 6 | function page_set_title(header_title, title) 7 | if header_title then 8 | if title == nil then title = header_title end 9 | ophal.title = title 10 | ophal.header_title = (header_title and header_title .. ' | ' or '') .. settings.site.name 11 | else 12 | ophal.header_title = settings.site.name 13 | end 14 | end 15 | 16 | function page_not_found() 17 | header('status', 404) 18 | page_set_title 'Page not found.' 19 | return '' 20 | end 21 | 22 | do 23 | local javascript = {} 24 | local order = {} 25 | local load_ophal_js = false 26 | add_js = {} 27 | 28 | setmetatable(add_js, { 29 | __call = function(t, options) 30 | load_ophal_js = true 31 | 32 | if options == nil then 33 | options = {} 34 | elseif type(options) == 'string' then 35 | options = {data = options} 36 | elseif type(options) == 'table' then 37 | options.data = options[1] 38 | options[1] = nil 39 | end 40 | 41 | local data = options.data 42 | options.data = nil 43 | 44 | local scope = options.scope and options.scope or 'header' 45 | 46 | if javascript[scope] == nil then javascript[scope] = {} end 47 | if order[scope] == nil then order[scope] = {} end 48 | 49 | if data ~= nil then 50 | if not javascript[scope][data] then 51 | order[scope][#order[scope] + 1] = data 52 | end 53 | javascript[scope][data] = options 54 | end 55 | end 56 | }) 57 | 58 | 59 | function init_js() 60 | add_js 'libraries/jquery.min.js' 61 | add_js 'libraries/ophal.js' 62 | add_js {type = 'settings', {base = base}} 63 | add_js {type = 'settings', namespace = 'locale', settings.locale} 64 | load_ophal_js = false 65 | 66 | for _, v in pairs(theme.settings.js or {}) do 67 | add_js(v) 68 | end 69 | end 70 | 71 | function get_js() 72 | if not load_ophal_js then 73 | return '' 74 | end 75 | 76 | local output = {} 77 | 78 | for scope, v in pairs(order) do 79 | output[scope] = {} 80 | for _, j in pairs(v) do 81 | local options = javascript[scope][j] 82 | if options ~= nil and options.type == 'settings' then 83 | output[scope][#output[scope] + 1] = ([=[ 90 | ]=]):format(options.namespace or 'core', json.encode(j) or '') 91 | elseif options ~= nil and options.type == 'inline' then 92 | output[scope][#output[scope] + 1] = ([=[ 97 | ]=]):format(j or '') 98 | elseif options ~= nil and options.type == 'external' then 99 | output[scope][#output[scope] + 1] = ([[ 100 | ]]):format(j or '') 101 | elseif is_file(j) then 102 | output[scope][#output[scope] + 1] = ([[ 103 | ]]):format(base.route, j, lfs.attributes(j, 'modification')) 104 | end 105 | end 106 | output[scope] = tcon(output[scope]) 107 | end 108 | return output 109 | end 110 | end 111 | 112 | do 113 | local css = {} 114 | 115 | function init_css() 116 | css[('themes/%s/style.css'):format(theme.name)] = {} 117 | 118 | for _, v in pairs(theme.settings.css or {}) do 119 | css[v:format(theme.name)] = {} 120 | end 121 | end 122 | 123 | function add_css(data, options) 124 | if options == nil then options = {} end 125 | if data ~= nil then 126 | css[data] = options 127 | end 128 | end 129 | 130 | function get_css() 131 | local output = {} 132 | for k, v in pairs(css) do 133 | if is_file(k) then 134 | output[1 + #output] = ([[ 135 | ]]):format(base.route, k, lfs.attributes(k, 'modification')) 136 | end 137 | end 138 | return tcon(output) 139 | end 140 | end 141 | 142 | do 143 | local head = {} 144 | 145 | function init_head() 146 | for k, v in pairs(theme.settings.head or {}) do 147 | head[k] = v 148 | end 149 | end 150 | 151 | function add_head(data) 152 | if data ~= nil then 153 | head[#head + 1] = data 154 | end 155 | end 156 | 157 | function get_head() 158 | return tcon(head, [[ 159 | 160 | ]]) 161 | end 162 | end 163 | 164 | function shutdown_ophal() 165 | -- call hook exit 166 | if module_invoke_all then 167 | module_invoke_all 'exit' 168 | end 169 | 170 | -- destroy session (phase end) 171 | if settings.sessionapi and session_write_close then 172 | session_write_close() 173 | end 174 | end 175 | 176 | function exit_ophal() 177 | shutdown_ophal() 178 | 179 | -- flush output buffer 180 | if settings.output_buffering then 181 | output_flush() 182 | end 183 | 184 | -- “I’m history! No, I’m mythology! Nah, I don’t care what I am; I’m free 185 | -- hee!” - Genie, Aladdin | Robin Williams 186 | server_exit() 187 | end 188 | 189 | --[[ 190 | Send the user to a different Ophal page. 191 | 192 | This issues an on-site HTTP redirect. The function makes sure the redirected 193 | URL is formatted correctly. 194 | 195 | This function ends the request; use it rather than a print theme('page') 196 | statement in your route callback. 197 | 198 | @param path 199 | A Drupal path or a full URL. 200 | @param query 201 | The query string component, if any. 202 | @param fragment 203 | The destination fragment identifier (named anchor). 204 | @param http_response_code 205 | Valid values for an actual "goto" as per RFC 2616 section 10.3 are: 206 | - 301 Moved Permanently (the recommended value for most redirects) 207 | - 302 Found (default in Drupal and PHP, sometimes used for spamming search 208 | engines) 209 | - 303 See Other 210 | - 304 Not Modified 211 | - 305 Use Proxy 212 | - 307 Temporary Redirect (an alternative to "503 Site Down for Maintenance") 213 | Note: Other values are defined by RFC 2616, but are rarely used and poorly 214 | supported. 215 | 216 | @see get_destination() 217 | ]] 218 | function goto(path, http_response_code, options) 219 | path = path or '' 220 | http_response_code = http_response_code or 302 221 | options = options or {} 222 | if options.absolute == nil then options.absolute = true end 223 | 224 | local dest_url 225 | 226 | dest_url = url(path, options) 227 | -- Remove newlines from the URL to avoid header injection attacks. 228 | dest_url = str_replace({'\n', '\r'}, '', dest_url) 229 | 230 | redirect(dest_url, http_response_code) 231 | 232 | exit_ophal() 233 | end 234 | 235 | --[[ 236 | Format given unix timestamp by system date format. 237 | ]] 238 | function format_date(uts, date_format) 239 | return date(date_format and date_format or settings.date_format, uts) 240 | end 241 | 242 | 243 | --[[ Format given file size in units. 244 | ]] 245 | do 246 | local units = {'B', 'KB', 'MB', 'GB', 'TB', 'PB'} 247 | 248 | function format_size(size) 249 | size = size or 0 250 | 251 | local unit, scale 252 | 253 | for k, v in pairs(units) do 254 | unit = v 255 | scale = k - 1 256 | if 1024^k > size then 257 | break 258 | end 259 | end 260 | size = round(size/1024^scale, 2) 261 | return ('%s %s'):format(size, unit) 262 | end 263 | end 264 | 265 | function get_global(key) 266 | return env[key] 267 | end 268 | 269 | function set_global(key, value) 270 | env[key] = value 271 | end 272 | -------------------------------------------------------------------------------- /includes/database/init.lua: -------------------------------------------------------------------------------- 1 | dbh = {} -- Database handlers 2 | 3 | local DBI, db_id, drivers = require 'DBI', 'default', {} 4 | 5 | function db_set_db_id(id) 6 | db_id = id 7 | end 8 | 9 | function db_connect() 10 | local err, driver 11 | local connection = settings.db[db_id] 12 | 13 | if connection == nil then return end 14 | 15 | if not connection.autocommit then connection.autocommit = true end 16 | 17 | dbh[db_id], err = DBI.Connect( 18 | connection.driver, 19 | connection.database, 20 | connection.username, 21 | connection.password, 22 | connection.host, 23 | connection.port 24 | ) 25 | 26 | if err then 27 | error(err) 28 | end 29 | 30 | drivers[db_id] = require('includes.database.' .. connection.driver:lower()) 31 | 32 | -- commit the transaction 33 | dbh[db_id]:autocommit(connection.autocommit) 34 | 35 | -- check status of the connection 36 | return dbh[db_id]:ping() 37 | end 38 | 39 | function db_query(query, ...) 40 | local err, sth 41 | 42 | if dbh[db_id] == nil then 43 | error 'No database connection' 44 | end 45 | 46 | -- prepare a query 47 | sth = assert(dbh[db_id]:prepare(query)) 48 | 49 | -- execute select with a bind variable 50 | _, err = sth:execute(...) 51 | 52 | if err then 53 | error(err) 54 | end 55 | 56 | return sth 57 | end 58 | 59 | function db_last_insert_id(...) 60 | return drivers[db_id].last_insert_id(...) 61 | end 62 | 63 | function db_limit() 64 | return drivers[db_id].limit() 65 | end 66 | -------------------------------------------------------------------------------- /includes/database/postgresql.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | function _M.last_insert_id(tbl_name, field) 4 | local sth, err, row 5 | 6 | sth, err = db_query('SELECT CURRVAL(?)', tbl_name .. '_' .. field .. '_seq') 7 | if err then 8 | return nil, err 9 | else 10 | row = sth:fetch() 11 | return row[1] 12 | end 13 | end 14 | 15 | function _M.limit() 16 | return ' OFFSET ? LIMIT ?' 17 | end 18 | 19 | return _M 20 | -------------------------------------------------------------------------------- /includes/database/sqlite3.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | function _M.last_insert_id(tbl_name) 4 | local sth, err, row 5 | 6 | sth, err = db_query('SELECT last_insert_rowid()') 7 | if err then 8 | return nil, err 9 | else 10 | row = sth:fetch() 11 | return row[1] 12 | end 13 | end 14 | 15 | function _M.limit() 16 | return ' LIMIT ?, ?' 17 | end 18 | 19 | return _M 20 | -------------------------------------------------------------------------------- /includes/debug.lua: -------------------------------------------------------------------------------- 1 | local print_r = seawolf.variable.print_r 2 | local temp_dir = seawolf.behaviour.temp_dir 3 | 4 | --[[ 5 | Wrapper of function print_r() from Nutria Seawolf. 6 | ]] 7 | function debug.print_r(val, return_) 8 | local result = ('
%s
'):format(seawolf.variable.print_r(val, true)) 9 | 10 | if return_ then 11 | return result 12 | end 13 | 14 | print(result) 15 | end 16 | 17 | function debug.log(msg) 18 | local fh = io.open(temp_dir() .. '/ophal.log', 'a+') 19 | if fh then 20 | return fh:write(("%s: %s\n"):format(os.date('%Y-%m-%d %H:%M:%S', os.time()), debug.print_r(msg, 1))) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /includes/form.lua: -------------------------------------------------------------------------------- 1 | local empty, tinsert, os_date = seawolf.variable.empty, table.insert, os.date 2 | local tsort, tconcat, os_time = table.sort, table.concat, os.time 3 | 4 | --[[ 5 | Form theme function. 6 | ]] 7 | function theme.form(variables) 8 | if variables == nil then variables = {} end 9 | 10 | local default_options = { 11 | ['accept-charset'] = 'UTF-8', 12 | method = 'get', 13 | action = variables.action, 14 | } 15 | 16 | module_invoke_all('form_alter', variables) 17 | 18 | return ('
%s
'):format( 19 | render_attributes(variables.attributes, default_options), 20 | theme{'elements', elements = variables.elements} 21 | ) 22 | end 23 | 24 | --[[ 25 | Form elements theme function. 26 | ]] 27 | function theme.elements(variables) 28 | if variables == nil then variables = {} end 29 | 30 | local elements 31 | local output = {''} 32 | local row = '' 33 | local row_nl = '' 34 | 35 | elements = variables.elements or {} 36 | variables.elements = nil 37 | 38 | tsort(elements, function(a, b) return (b.weight or 10) > (a.weight or 10) end) 39 | 40 | for _, v in pairs(elements) do 41 | if v[1] == 'hidden' then 42 | tinsert(output, theme(v)) 43 | elseif v.title then 44 | tinsert(output, row:format(v.title or '', theme(v))) 45 | else 46 | tinsert(output, row_nl:format(theme(v))) 47 | end 48 | end 49 | 50 | tinsert(output, '
%s:%s
%s
') 51 | 52 | return tconcat(output) 53 | end 54 | 55 | --[[ 56 | Textfield theme function. 57 | ]] 58 | function theme.textfield(variables) 59 | if variables == nil then variables = {} end 60 | 61 | if variables.attributes == nil then 62 | variables.attributes = {} 63 | end 64 | if variables.attributes.type == nil then 65 | variables.attributes.type = 'text' 66 | end 67 | 68 | return (''):format( 69 | render_attributes(variables.attributes), 70 | variables.value and ('value="%s" '):format(variables.value) or '' 71 | ) 72 | end 73 | 74 | --[[ 75 | Label theme function. 76 | ]] 77 | function theme.label(variables) 78 | if variables == nil then variables = {} end 79 | 80 | return (''):format( 81 | render_attributes(variables.attributes), 82 | variables.title 83 | ) 84 | end 85 | 86 | --[[ 87 | Button theme function. 88 | ]] 89 | function theme.button(variables) 90 | if variables == nil then variables = {} end 91 | 92 | return (''):format( 93 | render_attributes(variables.attributes), 94 | variables.value 95 | ) 96 | end 97 | 98 | --[[ 99 | Submit button theme function. 100 | ]] 101 | function theme.submit(variables) 102 | if variables == nil then variables = {} end 103 | 104 | return (''):format( 105 | render_attributes(variables.attributes), 106 | variables.value 107 | ) 108 | end 109 | 110 | --[[ 111 | Text area theme function. 112 | ]] 113 | function theme.textarea(variables) 114 | if variables == nil then variables = {} end 115 | 116 | return ('%s'):format( 117 | render_attributes(variables.attributes), 118 | variables.value and variables.value or '', 119 | variables.description and ('
%s
'):format(variables.description or '') or '' 120 | ) 121 | end 122 | 123 | --[[ 124 | Hidden value theme function. 125 | ]] 126 | function theme.hidden(variables) 127 | if variables == nil then variables = {} end 128 | 129 | return (''):format( 130 | render_attributes(variables.attributes), 131 | variables.value and ('value="%s" '):format(variables.value) or '' 132 | ) 133 | end 134 | 135 | --[[ 136 | Returns HTML for a select element. 137 | ]] 138 | function theme.select(variables) 139 | if variables == nil then variables = {} end 140 | if variables.class == nil then variables.class = {} end 141 | 142 | local options, order, choices, attributes 143 | 144 | options = variables.options 145 | variables.options = nil 146 | 147 | choices = variables.choices 148 | variables.choices = nil 149 | 150 | order = variables.order 151 | variables.order = nil 152 | 153 | variables.class['form-select'] = true 154 | variables.class = render_classes(variables.class) 155 | 156 | return (''):format( 157 | render_attributes(variables.attributes), 158 | theme{'select_options', options = options, order = order, choices = choices} 159 | ) 160 | end 161 | 162 | function theme.select_option(variables) 163 | local selected = variables.selected and ' selected="selected"' or '' 164 | return (''):format(variables.key, selected, variables.value) 165 | end 166 | 167 | --[[ 168 | Converts a select element's options array into HTML. 169 | ]] 170 | function theme.select_options(variables) 171 | if variables == nil then variables = {options = {}, choices = {}} end 172 | if variables.options == nil then variables.options = {} end 173 | if variables.choices == nil then variables.choices = {} end 174 | 175 | local options, order, choices, output = variables.options, variables.order, variables.choices, {} 176 | 177 | if not empty(order) then 178 | for k, key in pairs(order) do 179 | local selected = not empty(choices[key]) 180 | tinsert(output, theme{'select_option', key = key, value = options[key], selected = selected}) 181 | end 182 | else 183 | for k, v in pairs(options) do 184 | local selected = not empty(choices[k]) 185 | tinsert(output, theme{'select_option', key = k, value = v, selected = selected}) 186 | end 187 | end 188 | 189 | return tconcat(output) 190 | end 191 | 192 | function theme.checkbox(variables) 193 | if variables == nil then variables = {} end 194 | if variables.attributes == nil then variables.attributes = {} end 195 | 196 | variables.attributes.type = 'checkbox' 197 | variables.attributes.value = 1 198 | 199 | -- Unchecked checkbox has #value of integer 0. 200 | if not empty(variables.value) then 201 | variables.attributes.checked = 'checked' 202 | end 203 | 204 | return (''):format(render_attributes(variables.attributes)) 205 | end 206 | 207 | function theme.markup(variables) 208 | if variables == nil then variables = {} end 209 | 210 | return variables.value 211 | end 212 | 213 | function theme.progress(variables) 214 | if variables == nil then variables = {} end 215 | 216 | local value = variables.value or 0 217 | 218 | return ('
'):format( 219 | render_attributes(variables.attributes), 220 | value 221 | ) 222 | end 223 | 224 | function form_date_to_unixtime(date_string) 225 | local year, month, day, hour, min, sec = date_string:match("(%d+)-(%d+)-(%d+) (%d+):(%d+):(%d+)") 226 | 227 | return os.time{ 228 | year = year, 229 | month = month, 230 | day = day, 231 | hour = hour, 232 | min = min, 233 | sec = sec 234 | } 235 | end 236 | 237 | function theme.date(variables) 238 | if variables == nil then variables = {} end 239 | 240 | local offset = os_time() - os_time(os_date [[!*t]]) 241 | local text_value = os_date([[%Y-%m-%d %H:%M:%S]], variables.value) 242 | local offset_string = offset > -1 and os_date([[!+%H:%M]], offset) or os_date([[!-%H:%M]], offset*-1) 243 | 244 | return ([[%s 245 |
246 | Timezone: UTC %s]]):format( 247 | theme{'textfield', value = text_value, attributes = variables.attributes}, 248 | offset_string 249 | ) 250 | end 251 | -------------------------------------------------------------------------------- /includes/locale.lua: -------------------------------------------------------------------------------- 1 | local data = settings.locale or {} 2 | 3 | --[[ Attempt ot translate given text. 4 | ]] 5 | function t(text) 6 | return data[text] or text 7 | end 8 | -------------------------------------------------------------------------------- /includes/mobile.lua: -------------------------------------------------------------------------------- 1 | local seawolf = require 'seawolf'.__build('text', 'text.preg', 'variable') 2 | local _SERVER, pairs, rawget = _SERVER, pairs, rawget 3 | local strpos, empty = seawolf.text.strpos, seawolf.variable.empty 4 | local substr = seawolf.text.substr 5 | local tconcat, tinsert = table.concat, table.insert 6 | local preg = require 'seawolf.text.preg' 7 | 8 | --[[ 9 | Ophal mobile detect library. 10 | 11 | Based on PHP's Mobile_Detect - (c) Serban Ghita 2009-2012 12 | Version 2.0.7 - vic.stanciu@gmail.com, 3rd May 2012 13 | Site: https://code.google.com/p/php-mobile-detect/ 14 | ]] 15 | local detect = {} 16 | mobile.detect = detect 17 | 18 | local detectionRules = {} 19 | local userAgent = '' 20 | local accept = '' 21 | -- Assume the visitor has a desktop environment 22 | local isMobile_ = false 23 | local isTablet_ = false 24 | local phoneDeviceName = '' 25 | local tabletDevicename = '' 26 | local operatingSystemName = '' 27 | local userAgentName = '' 28 | 29 | -- List of mobile devices (phones) 30 | local phoneDevices = { 31 | iPhone = '(iPhone.*Mobile|iPod|iTunes)', 32 | BlackBerry = 'BlackBerry|rim[0-9]+', 33 | HTC = 'HTC|HTC.*(6800|8100|8900|A7272|S510e|C110e|Legend|Desire|T8282)|APX515CKT|Qtek9090', 34 | Nexus = 'Nexus One|Nexus S', 35 | DellStreak = 'Dell Streak', 36 | Motorola = '\bDroid\b.*Build|HRI39|MOT-|A1260|A1680|A555|A853|A855|A953|A955|A956|Motorola.*ELECTRIFY|Motorola.*i1|i867|i940|MB200|MB300|MB501|MB502|MB508|MB511|MB520|MB525|MB526|MB611|MB612|MB632|MB810|MB855|MB860|MB861|MB865|MB870|ME501|ME502|ME511|ME525|ME600|ME632|ME722|ME811|ME860|ME863|ME865|MT620|MT710|MT716|MT720|MT810|MT870|MT917|Motorola.*TITANIUM|WX435|WX445|XT300|XT301|XT311|XT316|XT317|XT319|XT320|XT390|XT502|XT530|XT531|XT532|XT535|XT603|XT610|XT611|XT615|XT681|XT701|XT702|XT711|XT720|XT800|XT806|XT860|XT862|XT875|XT882|XT883|XT894|XT909|XT910|XT912|XT928', 37 | Samsung = 'Samsung|GT-I9100|GT-I9000|GT-I9020|SCH-A310|SCH-A530|SCH-A570|SCH-A610|SCH-A630|SCH-A650|SCH-A790|SCH-A795|SCH-A850|SCH-A870|SCH-A890|SCH-A930|SCH-A950|SCH-A970|SCH-A990|SCH-I100|SCH-I110|SCH-I400|SCH-I405|SCH-I500|SCH-I510|SCH-I515|SCH-I600|SCH-I730|SCH-I760|SCH-I770|SCH-I830|SCH-I910|SCH-I920|SCH-LC11|SCH-N150|SCH-N300|SCH-R300|SCH-R400|SCH-R410|SCH-T300|SCH-U310|SCH-U320|SCH-U350|SCH-U360|SCH-U365|SCH-U370|SCH-U380|SCH-U410|SCH-U430|SCH-U450|SCH-U460|SCH-U470|SCH-U490|SCH-U540|SCH-U550|SCH-U620|SCH-U640|SCH-U650|SCH-U660|SCH-U700|SCH-U740|SCH-U750|SCH-U810|SCH-U820|SCH-U900|SCH-U940|SCH-U960|SCS-26UC|SGH-A107|SGH-A117|SGH-A127|SGH-A137|SGH-A157|SGH-A167|SGH-A177|SGH-A187|SGH-A197|SGH-A227|SGH-A237|SGH-A257|SGH-A437|SGH-A517|SGH-A597|SGH-A637|SGH-A657|SGH-A667|SGH-A687|SGH-A697|SGH-A697|SGH-A707|SGH-A717|SGH-A727|SGH-A737|SGH-A747|SGH-A767|SGH-A777|SGH-A797|SGH-A817|SGH-A827|SGH-A837|SGH-A847|SGH-A867|SGH-A877|SGH-A887|SGH-A897|SGH-A927|SGH-C207|SGH-C225|SGH-C417|SGH-D307|SGH-D347|SGH-D357|SGH-D407|SGH-D415|SGH-D807|SGH-E105|SGH-E315|SGH-E316|SGH-E317|SGH-E335|SGH-E635|SGH-E715|SGH-I577|SGH-I607|SGH-I617|SGH-I627|SGH-I637|SGH-I677|SGH-I717|SGH-I727|SGH-I777|SGH-I827|SGH-I847|SGH-I857|SGH-I896|SGH-I897|SGH-I907|SGH-I917|SGH-I927|SGH-I937|SGH-I997|SGH-N105|SGH-N625|SGH-P107|SGH-P207|SGH-P735|SGH-P777|SGH-Q105|SGH-R225|SGH-S105|SGH-S307|SGH-T109|SGH-T119|SGH-T139|SGH-T209|SGH-T219|SGH-T229|SGH-T239|SGH-T249|SGH-T259|SGH-T309|SGH-T319|SGH-T329|SGH-T339|SGH-T349|SGH-T359|SGH-T369|SGH-T379|SGH-T409|SGH-T429|SGH-T439|SGH-T459|SGH-T469|SGH-T479|SGH-T499|SGH-T509|SGH-T519|SGH-T539|SGH-T559|SGH-T589|SGH-T609|SGH-T619|SGH-T629|SGH-T639|SGH-T659|SGH-T669|SGH-T679|SGH-T709|SGH-T719|SGH-T729|SGH-T739|SGH-T749|SGH-T759|SGH-T769|SGH-T809|SGH-T819|SGH-T839|SGH-T919|SGH-T919|SGH-T929|SGH-T939|SGH-T939|SGH-T959|SGH-T989|SGH-V205|SGH-V206|SGH-X105|SGH-X426|SGH-X427|SGH-X475|SGH-X495|SGH-X497|SGH-X507|SGH-ZX10|SGH-ZX20|SPH-A120|SPH-A400|SPH-A420|SPH-A460|SPH-A500I|SPH-A560|SPH-A600|SPH-A620|SPH-A660|SPH-A700|SPH-A740|SPH-A760|SPH-A790|SPH-A800|SPH-A820|SPH-A840|SPH-A880|SPH-A900|SPH-A940|SPH-A960|SPH-D600|SPH-D700|SPH-D710|SPH-D720|SPH-I300|SPH-I325|SPH-I330|SPH-I350|SPH-I500|SPH-I600|SPH-I700|SPH-L700|SPH-M100|SPH-M220|SPH-M240|SPH-M300|SPH-M305|SPH-M320|SPH-M330|SPH-M350|SPH-M360|SPH-M370|SPH-M380|SPH-M510|SPH-M540|SPH-M550|SPH-M560|SPH-M570|SPH-M580|SPH-M610|SPH-M620|SPH-M630|SPH-M800|SPH-M810|SPH-M850|SPH-M900|SPH-M910|SPH-M920|SPH-M930|SPH-N200|SPH-N240|SPH-N300|SPH-N400|SPH-Z400|SWC-E100', 38 | Sony = 'E10i|SonyEricsson|SonyEricssonLT15iv', 39 | Asus = 'Asus.*Galaxy', 40 | Palm = 'PalmSource|Palm', -- avantgo|blazer|elaine|hiptop|plucker|xiino 41 | GenericPhone = '(mmp|pocket|psp|symbian|Smartphone|smartfon|treo|up.browser|up.link|vodafone|wap|nokia|Series40|Series60|S60|SonyEricsson|N900|PPC;|MAUI.*WAP.*Browser|LG-P500)', 42 | } 43 | 44 | -- List of tablet devices. 45 | local tabletDevices = { 46 | BlackBerryTablet = 'PlayBook|RIM Tablet', 47 | iPad = 'iPad|iPad.*Mobile', -- @todo: check for mobile friendly emails topic. 48 | Kindle = 'Kindle|Silk.*Accelerated', 49 | SamsungTablet = 'SAMSUNG.*Tablet|Galaxy.*Tab|GT-P1000|GT-P1010|GT-P6210|GT-P6800|GT-P6810|GT-P7100|GT-P7300|GT-P7310|GT-P7500|GT-P7510|SCH-I800|SCH-I815|SCH-I905|SGH-I777|SGH-I957|SGH-I987|SGH-T849|SGH-T859|SGH-T869|SGH-T989|SPH-D710|SPH-P100', 50 | HTCtablet = 'HTC Flyer|HTC Jetstream|HTC-P715a|HTC EVO View 4G|PG41200', 51 | MotorolaTablet = 'xoom|sholest|MZ615|MZ605|MZ505|MZ601|MZ602|MZ603|MZ604|MZ606|MZ607|MZ608|MZ609|MZ615|MZ616|MZ617', 52 | AsusTablet = 'Transformer|TF101', 53 | NookTablet = 'NookColor|nook browser|BNTV250A|LogicPD Zoom2', 54 | AcerTablet = 'Android.*(A100|A101|A200|A500|A501|A510|W500|W500P|W501|W501P)', 55 | YarvikTablet = 'Android.*(TAB210|TAB211|TAB224|TAB250|TAB260|TAB264|TAB310|TAB360|TAB364|TAB410|TAB411|TAB420|TAB424|TAB450|TAB460|TAB461|TAB464|TAB465|TAB467|TAB468)', 56 | GenericTablet = 'Tablet(?!.*PC)|ViewPad7|LG-V909|MID7015|BNTV250A|LogicPD Zoom2|\bA7EB\b|CatNova8|A1_07|CT704|CT1002|\bM721\b', 57 | } 58 | 59 | -- List of mobile Operating Systems 60 | local operatingSystems = { 61 | AndroidOS = '(android.*mobile|android(?!.*mobile))', 62 | BlackBerryOS = '(blackberry|rim tablet os)', 63 | PalmOS = '(avantgo|blazer|elaine|hiptop|palm|plucker|xiino)', 64 | SymbianOS = 'Symbian|SymbOS|Series60|Series40|\bS60\b', 65 | WindowsMobileOS = 'IEMobile|Windows Phone|Windows CE.*(PPC|Smartphone)|MSIEMobile|Window Mobile|XBLWP7', 66 | iOS = '(iphone|ipod|ipad)', 67 | FlashLiteOS = '', 68 | JavaOS = '', 69 | NokiaOS = '', 70 | webOS = '', 71 | badaOS = '\bBada\b', 72 | BREWOS = '', 73 | } 74 | 75 | -- List of mobile User Agents 76 | local userAgents = { 77 | Chrome = '\bCrMo\b|Chrome/[.0-9]* Mobile', 78 | Dolfin = '\bDolfin\b', 79 | Opera = 'Opera.*Mini|Opera.*Mobi', 80 | Skyfire = 'skyfire', 81 | IE = 'IEMobile', 82 | Firefox = 'fennec|firefox.*maemo|(Mobile|Tablet).*Firefox|Firefox.*Mobile', 83 | Bolt = 'bolt', 84 | TeaShark = 'teashark', 85 | Blazer = 'Blazer', 86 | Safari = 'Mobile.*Safari|Safari.*Mobile', 87 | Midori = 'midori', 88 | GenericBrowser = 'NokiaBrowser|OviBrowser|SEMC.*Browser', 89 | } 90 | 91 | function detect.getRules() 92 | return detectionRules 93 | end 94 | 95 | --[[ 96 | Private method that does the detection of the 97 | mobile devices. 98 | 99 | @param type $key 100 | @return boolean|null 101 | ]] 102 | local function detect_(key) 103 | if key == nil then key = '' end 104 | 105 | local _rules = {} 106 | 107 | if empty(key) then 108 | -- Begin general search 109 | for _, _regex in pairs(detectionRules) do 110 | if not empty(_regex) then 111 | if not empty(preg.match(_regex, userAgent, nil, nil, nil, 'is')) then 112 | isMobile_ = true 113 | return true 114 | end 115 | end 116 | end 117 | return false 118 | else 119 | -- Search for a certain key. 120 | -- Make the keys lowecase so we can match: isIphone(), isiPhone(), isiphone(), etc. 121 | key = key:lower() 122 | for k, v in pairs(detectionRules) do 123 | _rules[k:lower()] = v 124 | end 125 | 126 | if _rules[key] ~= nil then 127 | if empty(_rules[key]) then 128 | return nil 129 | end 130 | if not empty(preg.match(_rules[key], userAgent, nil, nil, nil, 'is')) then 131 | isMobile_ = true 132 | return true 133 | else 134 | return false 135 | end 136 | else 137 | return ('Method %s is not defined'):format(key) 138 | end 139 | 140 | return false 141 | end 142 | end 143 | 144 | --[[ 145 | Check if the device is mobile. 146 | Returns true if any type of mobile device detected, including special ones 147 | @return bool 148 | ]] 149 | function detect.isMobile() 150 | return isMobile_ 151 | end 152 | 153 | --[[ 154 | Check if the device is a tablet. 155 | Return true if any type of tablet device is detected. 156 | @return boolean 157 | ]] 158 | function detect.isTablet() 159 | for _, _regex in pairs(tabletDevices) do 160 | if not empty(preg.match(_regex, userAgent, nil, nil, nil, 'is')) then 161 | isTablet_ = true 162 | return true 163 | end 164 | end 165 | 166 | return false 167 | end 168 | 169 | --[[ Construct ]] 170 | -- Merge all rules together 171 | for k, v in pairs(phoneDevices) do 172 | detectionRules[k] = v 173 | end 174 | for k, v in pairs(tabletDevices) do 175 | detectionRules[k] = v 176 | end 177 | for k, v in pairs(operatingSystems) do 178 | detectionRules[k] = v 179 | end 180 | for k, v in pairs(userAgents) do 181 | detectionRules[k] = v 182 | end 183 | userAgent = _SERVER 'HTTP_USER_AGENT' 184 | accept = _SERVER 'HTTP_ACCEPT' 185 | 186 | if 187 | _SERVER 'HTTP_X_WAP_PROFILE' ~= nil or 188 | _SERVER 'HTTP_X_WAP_CLIENTID' ~= nil or 189 | _SERVER 'HTTP_WAP_CONNECTION' ~= nil or 190 | _SERVER 'HTTP_PROFILE' ~= nil or 191 | _SERVER 'HTTP_X_OPERAMINI_PHONE_UA' ~= nil or -- Reported by Nokia devices (eg. C3) 192 | _SERVER 'HTTP_X_NOKIA_IPADDRESS' ~= nil or 193 | _SERVER 'HTTP_X_NOKIA_GATEWAY_ID' ~= nil or 194 | _SERVER 'HTTP_X_ORANGE_ID' ~= nil or 195 | _SERVER 'HTTP_X_VODAFONE_3GPDPCONTEXT' ~= nil or 196 | _SERVER 'HTTP_X_HUAWEI_USERID' ~= nil or 197 | _SERVER 'HTTP_UA_OS' ~= nil or -- Reported by Windows Smartphones 198 | (_SERVER 'HTTP_UA_CPU' ~= nil and _SERVER 'HTTP_UA_CPU' == 'ARM') -- Seen this on a HTC 199 | then 200 | isMobile_ = true 201 | elseif not empty(accept) and (strpos(accept, 'text/vnd.wap.wml') ~= false or strpos(accept, 'application/vnd.wap.xhtml+xml') ~= false) then 202 | isMobile_ = true 203 | else 204 | detect_() 205 | end 206 | 207 | setmetatable(mobile.detect, { 208 | __index = function(t, name) 209 | local key, value 210 | 211 | value = rawget(t, name) 212 | if value ~= nil then 213 | return value 214 | end 215 | 216 | if name ~= nil then 217 | key = substr(name, 3) 218 | else 219 | key = '' 220 | end 221 | return function () return detect_(key) end 222 | end, 223 | }) 224 | -------------------------------------------------------------------------------- /includes/module.lua: -------------------------------------------------------------------------------- 1 | local xtable = seawolf.contrib.seawolf_table 2 | 3 | do 4 | local mt = { 5 | register = function(t, module_name, module_definition) 6 | ophal.modules[module_name] = module_definition 7 | end 8 | } 9 | mt.__index = function(t, k) 10 | if mt[k] ~= nil then 11 | return mt[k] 12 | end 13 | end 14 | setmetatable(ophal.modules, mt) 15 | end 16 | 17 | do 18 | local order, group 19 | local list = xtable{'system'} 20 | 21 | --[[ Return the list of active modules by weight and name 22 | ]] 23 | function module_list() 24 | if nil == order then 25 | local rawset = rawset 26 | 27 | order, group = xtable(), xtable() 28 | 29 | -- Force system module to stay first ALWAYS 30 | settings.modules.system = nil 31 | 32 | -- Group modules by their weight 33 | for name, weight in pairs(settings.modules) do 34 | -- Ignore disabled modules 35 | if weight == false then break end 36 | 37 | if weight == true then 38 | weight = 1 39 | end 40 | 41 | if nil == group[weight] then 42 | rawset(group, weight, xtable{name}) 43 | order:append(weight) 44 | else 45 | group[weight]:append(name) 46 | end 47 | end 48 | 49 | -- Sort weights 50 | order:sort() 51 | 52 | -- Build list of module names 53 | for k, weight in pairs(order) do 54 | -- Sort alphabetically 55 | group[weight]:sort() 56 | -- Add modules in current group to the list of names 57 | for j, name in pairs(group[weight]) do 58 | list:append(name) 59 | end 60 | end 61 | end 62 | 63 | return list 64 | end 65 | end 66 | 67 | function module_invoke_all(hook, ...) 68 | local err 69 | local result, r = {} 70 | 71 | for _, name in pairs(module_list()) do 72 | local m = ophal.modules[name] 73 | if m and m[hook] then 74 | r, err = m[hook](...) -- call hook implementation 75 | if err then 76 | return nil, err 77 | end 78 | if type(r) == 'table' then 79 | for k, v in pairs(r) do 80 | v.module = name -- register module name 81 | result[k] = v 82 | end 83 | elseif r then 84 | table.insert(result, r) 85 | end 86 | end 87 | end 88 | 89 | return result 90 | end 91 | 92 | function module_load(name) 93 | local status, err = pcall(require, 'modules.' .. name .. '.init') 94 | if not status then 95 | print('bootstrap: ' .. err) 96 | end 97 | end 98 | 99 | function module_load_all() 100 | for _, name in pairs(module_list()) do 101 | module_load(name) 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /includes/pager.lua: -------------------------------------------------------------------------------- 1 | local tconcat = table.concat 2 | local seawolf = require 'seawolf'.__build('contrib') 3 | 4 | function pager_url(path, page, selector) 5 | local result = seawolf.contrib.seawolf_table() 6 | 7 | result:append(url(path)) 8 | 9 | if page > 1 then 10 | result:append{'?page=', page} 11 | if selector then 12 | result:append{'#', selector} 13 | end 14 | end 15 | 16 | return result:concat() 17 | end 18 | 19 | function pager(route, num_pages, current_page, selector) 20 | if nil == current_page then 21 | current_page = 1 22 | elseif type(current_page) ~= 'number' then 23 | current_page = tonumber(current_page) 24 | end 25 | 26 | local pages = {} 27 | 28 | if num_pages <= 1 then 29 | return pages 30 | end 31 | -- Link to previous page 32 | if current_page > 1 then 33 | pages[#pages + 1] = l('previous', pager_url(route, current_page - 1, selector), { 34 | external = true, 35 | attributes = {rel = 'prev'}, 36 | }) 37 | end 38 | 39 | -- Build links to all pages 40 | for page = 1,num_pages do 41 | pages[#pages + 1] = page ~= current_page and 42 | l(page, pager_url(route, page, selector), {external = true}) or 43 | page 44 | end 45 | 46 | -- Link to next page 47 | if current_page < num_pages then 48 | pages[#pages + 1] = l('next', pager_url(route, current_page + 1, selector), { 49 | external = true, 50 | attributes = {rel = 'next'}, 51 | }) 52 | end 53 | 54 | return pages 55 | end 56 | 57 | function theme.pager(variables) 58 | if variables == nil then variables = {} end 59 | 60 | local pages = variables.pages ~= nil and variables.pages or {} 61 | 62 | if #pages > 0 then 63 | return '' 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /includes/route.lua: -------------------------------------------------------------------------------- 1 | if not ophal.aliases.source then ophal.aliases.source = {} end 2 | if not ophal.aliases.alias then ophal.aliases.alias = {} end 3 | if not ophal.redirects.source then ophal.redirects.source = {} end 4 | if not ophal.redirects.target then ophal.redirects.target = {} end 5 | 6 | local explode = seawolf.text.explode 7 | local table_shift = seawolf.contrib.table_shift 8 | local aliases = ophal.aliases 9 | local redirects = ophal.redirects 10 | local route_set_title, pcall = route_set_title, pcall 11 | local empty = seawolf.variable.empty 12 | 13 | function route_register_alias(source, alias) 14 | aliases.source[source] = alias 15 | aliases.alias[alias] = source 16 | end 17 | 18 | function route_aliases_load() 19 | local alias 20 | local rs, err = db_query 'SELECT * FROM route_alias' 21 | for row in rs:rows(true) do 22 | alias = row.alias 23 | if (row.language or 'all') ~= 'all' and settings.route_aliases_prepend_language then 24 | alias = row.language .. '/' .. row.alias 25 | end 26 | route_register_alias(row.source, alias) 27 | end 28 | end 29 | 30 | function route_read_alias(id) 31 | local rs = db_query('SELECT * FROM route_alias WHERE id = ?', id) 32 | return rs:fetch(true) 33 | end 34 | 35 | function route_create_alias(entity) 36 | if empty(entity.language) then 37 | entity.language = 'all' 38 | end 39 | 40 | local rs, err 41 | 42 | if entity.type == nil then entity.type = 'route_alias' end 43 | 44 | if entity.id then 45 | rs, err = db_query([[ 46 | INSERT INTO route_alias(id, source, alias, language) 47 | VALUES(?, ?, ?, ?)]], 48 | entity.id, 49 | entity.source, 50 | entity.alias, 51 | entity.language 52 | ) 53 | else 54 | rs, err = db_query([[ 55 | INSERT INTO route_alias(source, alias, language) 56 | VALUES(?, ?, ?)]], 57 | entity.source, 58 | entity.alias, 59 | entity.language 60 | ) 61 | entity.id = db_last_insert_id('route_alias', 'id') 62 | end 63 | 64 | if not err then 65 | module_invoke_all('entity_after_save', entity) 66 | end 67 | return entity.id, err 68 | end 69 | 70 | function route_update_alias(id, entity) 71 | local keys, placeholders = {}, {} 72 | local record = route_read_alias(id) 73 | for _, v in pairs{'source', 'alias', 'language'} do 74 | record[v] = entity[v] 75 | end 76 | return db_query('UPDATE route_alias SET source = ?, alias = ?, language = ? WHERE id = ?', record.source, record.alias, record.language, id) 77 | end 78 | 79 | function route_delete_alias(id) 80 | return db_query('DELETE FROM route_alias WHERE id = ?', id) 81 | end 82 | 83 | function route_redirect() 84 | local redirect = ophal.redirects.source[request_path()] 85 | if redirect then 86 | goto(redirect[1], redirect[2]) 87 | end 88 | end 89 | 90 | function route_register_redirect(source, target, http_code) 91 | redirects.source[source] = {target, http_code} 92 | redirects.target[target] = source 93 | end 94 | 95 | function route_redirects_load() 96 | local target 97 | local rs, err = db_query 'SELECT * FROM route_redirect' 98 | for row in rs:rows(true) do 99 | target = row.target 100 | if (row.language or 'all') ~= 'all' and settings.route_redirects_prepend_language then 101 | target = row.language .. '/' .. target 102 | end 103 | route_register_redirect(row.source, target, row.type) 104 | end 105 | end 106 | 107 | function route_create_redirect(entity) 108 | if empty(entity.language) then 109 | entity.language = 'all' 110 | end 111 | 112 | local rs, err 113 | 114 | if entity.type == nil then entity.type = 'route_redirect' end 115 | 116 | if entity.id then 117 | rs, err = db_query([[ 118 | INSERT INTO route_redirect(id, source, alias, language, type) 119 | VALUES(?, ?, ?, ?, ?)]], 120 | entity.id, 121 | entity.source, 122 | entity.target, 123 | entity.language, 124 | entity.type 125 | ) 126 | else 127 | rs, err = db_query([[ 128 | INSERT INTO route_redirect(source, target, language, type) 129 | VALUES(?, ?, ?, ?)]], 130 | entity.source, 131 | entity.target, 132 | entity.language, 133 | entity.type 134 | ) 135 | entity.id = db_last_insert_id('route_redirect', 'id') 136 | end 137 | 138 | if not err then 139 | module_invoke_all('entity_after_save', entity) 140 | end 141 | return entity.id, err 142 | end 143 | 144 | function route_read_redirect(id) 145 | local rs = db_query('SELECT * FROM route_redirect WHERE id = ?', id) 146 | return rs:fetch(true) 147 | end 148 | 149 | function route_update_redirect(id, entity) 150 | local keys, placeholders = {}, {} 151 | local record = route_read_redirect(id) 152 | for _, v in pairs{'source', 'alias', 'language', 'type'} do 153 | record[v] = entity[v] 154 | end 155 | return db_query('UPDATE route_redirect SET source = ?, target = ?, language = ?, type = ? WHERE id = ?', record.source, record.target, record.language, record.type, id) 156 | end 157 | 158 | function route_delete_redirect(id) 159 | return db_query('DELETE FROM route_redirect WHERE id = ?', id) 160 | end 161 | 162 | do 163 | local arguments 164 | 165 | function route_arg(index) 166 | local source, rp 167 | 168 | index = index + 1 169 | if arguments == nil then 170 | rp = request_path() 171 | source = aliases.alias[rp] 172 | if source then 173 | rp = source 174 | end 175 | arguments = explode('/', rp ~= '' and rp or settings.site.frontpage) 176 | end 177 | 178 | return arguments[index] 179 | end 180 | end 181 | 182 | local slash = settings.slash 183 | 184 | do 185 | local route_tree, route 186 | function init_route() 187 | local alias 188 | 189 | if route_tree == nil and route == nil then 190 | route_tree, route = {} 191 | 192 | -- build route tree 193 | for i = 1,8 do 194 | a = route_arg(i - 1) 195 | if a == nil or a == '' then 196 | break 197 | else 198 | route = (route or '') .. (route and slash or '') .. (a or '') 199 | table.insert(route_tree, route) 200 | end 201 | end 202 | if not #route_tree then 203 | error 'Route system error!' 204 | end 205 | end 206 | return route_tree, route 207 | end 208 | end 209 | 210 | function route_build_handler(handler, module_name) 211 | local callback 212 | local known_callbacks = {'access_callback', 'page_callback'} 213 | 214 | handler.module = module_name -- register module name 215 | 216 | for _, v in pairs(known_callbacks) do 217 | callback = handler[v] 218 | if type(callback) == 'string' then 219 | handler[v] = { 220 | callback, 221 | module = module_name, 222 | } 223 | elseif type(callback) == 'table' then 224 | handler[v] = { 225 | callback[1], 226 | module = callback.module or module_name, 227 | arguments = table_shift(callback), 228 | } 229 | end 230 | end 231 | end 232 | 233 | function route_build_routes() 234 | local err 235 | local routes, r = {} 236 | 237 | for name, m in pairs(ophal.modules) do 238 | if m.route then 239 | r, err = m.route() -- call hook implementation 240 | if err then 241 | return nil, err 242 | end 243 | if type(r) == 'table' then 244 | for k, v in pairs(r) do 245 | route_build_handler(v, name) 246 | routes[k] = v 247 | end 248 | elseif r then 249 | table.insert(routes, r) 250 | end 251 | end 252 | end 253 | 254 | return routes 255 | end 256 | 257 | --[[ Generates an internal or external URL. 258 | 259 | Params: 260 | options (optional): A table with the following elements: 261 | 'alias': Whether the given path is a URL alias already. 262 | 'absolute': Whether to force the output to be an absolute link. 263 | 'external': Whether the given path is an external URL. 264 | ]] 265 | function url(route, options) 266 | if options == nil then options = {} end 267 | if route == nil then route = '' end 268 | 269 | local alias 270 | 271 | if not (options.alias or options.external) then 272 | alias = aliases.source[route] 273 | if alias then 274 | route = alias 275 | end 276 | end 277 | 278 | if options.external then 279 | return route 280 | end 281 | 282 | return (options.absolute and base.system_root or '') .. base.route .. route 283 | end 284 | 285 | function l(text, route, options) 286 | if options == nil then options = {} end 287 | 288 | local attributes = options.attributes or {} 289 | options.attributes = nil 290 | 291 | return theme{'a', 292 | text = text, 293 | route = url(route, options), 294 | attributes = attributes, 295 | } 296 | end 297 | 298 | --[[ 299 | Look for route handlers in route_tree. 300 | ]] 301 | function route_get_handler() 302 | local a, route, aliased 303 | local routes, handler = ophal.routes 304 | local route_tree = init_route() 305 | 306 | for i = 1, #route_tree do 307 | a = #route_tree - (i - 1) -- start from bottom 308 | route = route_tree[a] -- get route from stack 309 | handler = routes[route] -- lookup handler 310 | if handler then 311 | handler.route = route 312 | break 313 | end 314 | end 315 | 316 | if not handler then 317 | handler = { 318 | error = 404, 319 | title = 'Page not found', 320 | content = 'The requested page could not be found.', 321 | format = 'html', 322 | } 323 | end 324 | 325 | if handler.format == nil then 326 | handler.format = 'html' -- default output format 327 | end 328 | 329 | module_invoke_all('route_validate_handler', handler) 330 | 331 | return handler 332 | end 333 | 334 | function route_execute_callback(handler, callback) 335 | local func, result 336 | local status = true 337 | 338 | if handler[callback] then 339 | func = ophal.modules[handler[callback].module][handler[callback][1]] 340 | status, result = pcall(func, unpack(handler[callback].arguments or {})) 341 | if not status then 342 | result = ("module '%s': %s"):format(handler.module, result) 343 | end 344 | end 345 | 346 | return status, result 347 | end 348 | 349 | function route_execute_active_handler() 350 | local handler, status, content 351 | 352 | -- Execute handler 353 | handler = route_get_handler() 354 | if handler.error then 355 | header('status', handler.error) 356 | content = handler.content 357 | page_set_title(handler.title) 358 | else 359 | page_set_title(handler.title) -- allow later override 360 | status, content = route_execute_callback(handler, 'page_callback') 361 | end 362 | 363 | -- Render content 364 | print_t{handler.format, 365 | status = status, 366 | header_title = ophal.header_title, 367 | title = ophal.title, 368 | content = content, 369 | head = get_head(), 370 | javascript = get_js(), 371 | css = get_css(), 372 | regions = theme_get_regions(), 373 | } 374 | end 375 | 376 | function route_forbidden(variables) 377 | if nil == variables then variables = {} end 378 | 379 | -- Defaults 380 | variables.header_title = variables.header_title or 'Access denied' 381 | variables.title = variables.title or 'Access denied' 382 | variables.body = variables.body or 'You have not access to this page.' 383 | 384 | module_invoke_all('route_forbidden_alter', variables) 385 | 386 | header('status', 401) 387 | page_set_title(variables.header_title, variables.title) 388 | 389 | return variables.body 390 | end 391 | 392 | function route_not_found(variables) 393 | if nil == variables then variables = {} end 394 | 395 | -- Defaults 396 | variables.header_title = variables.header_title or 'Page not found' 397 | variables.title = variables.title or 'Page not found' 398 | variables.body = variables.body or body 'The requested page could not be found.' 399 | 400 | module_invoke_all('route_not_found_alter', variables) 401 | 402 | header('status', 404) 403 | page_set_title(variables.header_title, variables.title) 404 | 405 | return variables.body 406 | end 407 | -------------------------------------------------------------------------------- /includes/server/cgi.lua: -------------------------------------------------------------------------------- 1 | local io_write, buffer = io.write, env.output_buffer 2 | local time, date, exit = os.time, os.date, os.exit 3 | local tinsert, explode = table.insert, seawolf.text.explode 4 | local empty, ltrim = seawolf.variable.empty, seawolf.text.ltrim 5 | local trim, dirname = seawolf.text.trim, seawolf.fs.dirname 6 | local basename = seawolf.fs.basename 7 | local rtrim, unescape = seawolf.text.rtrim, socket.url.unescape 8 | local tconcat, lower = table.concat, string.lower 9 | 10 | ophal.raw_cookies = _SERVER 'HTTP_COOKIE' 11 | 12 | -- Output functions 13 | -- Make sure to print headers on the first output 14 | write = function (s) 15 | io.write = io_write 16 | write = io_write 17 | ophal.header:print() 18 | write "\n" 19 | write(s) 20 | end 21 | io.write = write 22 | 23 | do 24 | local exit_orig = exit 25 | exit = function (code) 26 | os.exit = exit_orig 27 | exit = exit_orig 28 | ophal.header:print() 29 | exit_orig(code) 30 | end 31 | os.exit = exit 32 | end 33 | 34 | -- Headers handler 35 | ophal.header = { 36 | sent = false, 37 | data = {}, 38 | set = function (t, header) 39 | local replace 40 | 41 | local name = header[1] 42 | local value = header[2] 43 | if header[3] ~= nil then 44 | replace = header[3] 45 | else 46 | replace = true 47 | end 48 | 49 | local headers = t.data 50 | 51 | if not empty(name) and type(name) == 'string' and 52 | (type(value) == 'string' or type(value) == 'number' or type(value) == 'function') 53 | then 54 | name = lower(name) 55 | if name == 'status' then 56 | replace = true -- always replace status header 57 | end 58 | if replace then 59 | headers[name] = {value} 60 | else 61 | if headers[name] == nil then 62 | headers[name] = {} 63 | end 64 | tinsert(headers[name], value) 65 | end 66 | end 67 | end, 68 | print = function (t) 69 | if not t.sent then 70 | for n, d in pairs(t.data) do 71 | for _, v in pairs(d) do 72 | if type(v) == 'function' then 73 | v() 74 | else 75 | io_write(([[%s: %s 76 | ]]):format(n, v)) 77 | end 78 | end 79 | end 80 | t.sent = true 81 | end 82 | end 83 | } 84 | 85 | function header(...) 86 | ophal.header:set{...} 87 | end 88 | 89 | --[[ 90 | Redirect to raw destination URL. 91 | ]] 92 | function redirect(dest_url, http_response_code) 93 | header('status', http_response_code) 94 | header('location', dest_url) 95 | header('connection', 'close') 96 | write '' 97 | end 98 | 99 | function request_get_body() 100 | return io.read '*a' 101 | end 102 | 103 | function server_exit() 104 | os.exit() 105 | end 106 | -------------------------------------------------------------------------------- /includes/server/init.lua: -------------------------------------------------------------------------------- 1 | local buffer = env.output_buffer 2 | local time, date, exit = os.time, os.date, os.exit 3 | local tinsert, explode = table.insert, seawolf.text.explode 4 | local empty, ltrim = seawolf.variable.empty, seawolf.text.ltrim 5 | local base, trim, dirname = base, seawolf.text.trim, seawolf.fs.dirname 6 | local basename, parse_date = seawolf.fs.basename, seawolf.contrib.parse_date 7 | local rtrim, unescape = seawolf.text.rtrim, socket.url.unescape 8 | local tconcat, lower = table.concat, string.lower 9 | 10 | --[[ Ophal's print function. 11 | 12 | It is the unique Ophal's output function, *write() is for internal use only*. 13 | ]] 14 | function print(s) 15 | write(tostring(s)) 16 | end 17 | 18 | function echo(...) 19 | for _, v in pairs({...}) do 20 | write(tostring(v)) 21 | end 22 | end 23 | 24 | -- Default headers 25 | header('content-type', 'text/html; charset=utf-8') 26 | if ophal.version then 27 | header('x-powered-by', ophal.version) 28 | end 29 | 30 | -- Browser micro cache control 31 | if settings.micro_cache and _SERVER 'HTTP_IF_MODIFIED_SINCE' ~= nil then 32 | local parsed = parse_date(_SERVER 'HTTP_IF_MODIFIED_SINCE') 33 | local last_access = tonumber(('%s%s%s%s%s%s'):format(parsed.year, parsed.month, parsed.day, parsed.hours, parsed.minutes, parsed.seconds)) 34 | local now = tonumber(os.date('%Y%m%d%H%M%S', time())) 35 | if last_access + 5 >= now then 36 | header('status', '304 Not Modified') 37 | header('cache-control', 'must-revalidate') 38 | print '' 39 | exit() 40 | end 41 | end 42 | 43 | -- Redirect to mobile domain name 44 | if settings.mobile then 45 | local domain_name = settings.mobile.domain_name 46 | local uri = _SERVER 'REQUEST_URI' 47 | if settings.mobile.redirect and mobile.detect.isMobile() and _SERVER 'HTTP_HOST' ~= domain_name then 48 | local redirect_url = domain_name .. (_SERVER 'REQUEST_URI' or '') 49 | header('Location', 'http://' .. redirect_url) 50 | print(('Redirecting to http://%s.'):format(redirect_url, redirect_url)) 51 | os.exit() 52 | end 53 | end 54 | 55 | -- Set headers for dynamic content 56 | header('expires', 'Sun, 19 Jun 2011 23:09:50 GMT') 57 | header('last-modified', date('!%a, %d %b %Y %X GMT')) 58 | header('cache-control', 'store, no-cache, must-revalidate, post-check=0, pre-check=0') 59 | header('Keep-Alive', 'timeout=15, max=90') 60 | 61 | --[[ 62 | Since _SERVER['REQUEST_URI'] is only available on Apache, we 63 | generate an equivalent using other environment variables. 64 | 65 | Copied and adapted from Drupal 8.x request_uri(). 66 | ]] 67 | function request_uri(omit_query_string) 68 | local uri 69 | 70 | if _SERVER 'REQUEST_URI' ~= nil then 71 | uri = _SERVER 'REQUEST_URI' 72 | else 73 | if _SERVER 'QUERY_STRING' ~= nil then 74 | uri = _SERVER 'SCRIPT_NAME' .. '?' .. _SERVER 'QUERY_STRING' 75 | else 76 | uri = _SERVER 'SCRIPT_NAME' or '' 77 | end 78 | end 79 | -- Prevent multiple slashes to avoid cross site requests via the FAPI. 80 | uri = '/'.. ltrim(uri, '/') 81 | 82 | if omit_query_string then 83 | for _, v in pairs(explode('?', uri)) do 84 | if v ~= '' then 85 | return v 86 | end 87 | end 88 | end 89 | 90 | return uri 91 | end 92 | 93 | do 94 | local path 95 | 96 | --[[ 97 | Returns the requested URL path of the page being viewed. 98 | 99 | Examples: 100 | - http://example.com/article/306 returns "article/306". 101 | - http://example.com/ophalfolder/article/306 returns "article/306" while 102 | base.route() returns "/ophalfolder/". 103 | - http://example.com/path/alias (which is a path alias for article/306) 104 | returns "path/alias" as opposed to the internal path. 105 | - http://example.com/index.cgi returns an empty string, meaning: front page. 106 | - http://example.com/index.cgi?page=1 returns an empty string. 107 | 108 | Copied and adapted from Drupal 8.x request_path(). 109 | ]] 110 | function request_path() 111 | local request_path, base_route_len, script 112 | 113 | if path ~= nil then 114 | return path 115 | end 116 | 117 | -- Get the part of the URI between the base path of the Drupal installation 118 | -- and the query string, and unescape it. 119 | request_path = request_uri(true) 120 | base_route_len = rtrim(dirname(_SERVER 'SCRIPT_NAME'), '\\/'):len() 121 | path = unescape(request_path):sub(base_route_len + 1) 122 | 123 | -- Depending on server configuration, the URI might or might not include the 124 | -- script name. For example, the front page might be accessed as 125 | -- http://example.com or as http://example.com/index.cgi, and the "user" 126 | -- page might be accessed as http://example.com/user or as 127 | -- http://example.com/index.cgi/user. Strip the script name from $path. 128 | script = basename(_SERVER 'SCRIPT_NAME') 129 | if path == script then 130 | path = '' 131 | elseif path:find(script .. '/') == 0 then 132 | path = substr(path, strlen(script) + 1) 133 | end 134 | 135 | -- Extra slashes can appear in URLs or under some conditions, added by 136 | -- the web server, so normalize. 137 | path = trim(path, '/') 138 | 139 | return path 140 | end 141 | end 142 | 143 | -- Build base URL, system_root, route and path 144 | function build_base() 145 | if not empty((settings.site or {}).scheme) then 146 | base.scheme = settings.site.scheme 147 | else 148 | base.scheme = (_SERVER 'HTTPS' ~= nil and _SERVER 'HTTPS' == 'on') and 'https' or 'http' 149 | end 150 | base.system_root = base.scheme .. '://' .. ((settings.site or {}).domain_name or _SERVER 'HTTP_HOST' or 'default') 151 | base.url = base.system_root 152 | base.path = request_path() 153 | 154 | local dir = seawolf.text.trim(seawolf.fs.dirname(_SERVER 'SCRIPT_NAME' or '/index.cgi'), [[\,/]]) 155 | if dir ~= '' then 156 | base.route = '/' .. dir 157 | base.url = base.url .. base.route 158 | base.route = base.route .. '/' 159 | end 160 | end 161 | 162 | -- Parse query string 163 | local list = explode('&', _SERVER 'QUERY_STRING' or '') 164 | 165 | local parsed = {} 166 | if list then 167 | local tmp, key, value 168 | for _, v in pairs(list) do 169 | if #v > 0 then 170 | tmp = explode('=', v) 171 | key = unescape((tmp[1] or ''):gsub('+', ' ')) 172 | value = unescape((tmp[2] or ''):gsub('+', ' ')) 173 | parsed[key] = value 174 | end 175 | end 176 | end 177 | _GET = parsed 178 | 179 | -- output buffering 180 | do 181 | local write_orig = write 182 | local exit_orig = exit 183 | if settings.output_buffering then 184 | write = function (s) 185 | local type_ = type(s) 186 | if type_ ~= 'string' then 187 | s = ('(%s)'):format(type_) 188 | end 189 | tinsert(buffer, #buffer + 1, s) 190 | end 191 | io.write = write 192 | exit = function (code) 193 | output_flush() 194 | exit_orig(code) 195 | end 196 | os.exit = exit 197 | local error_orig = error 198 | error = function (s) 199 | output_flush() 200 | error_orig(s) 201 | end 202 | end 203 | 204 | function output_clean() 205 | for k, v in pairs(buffer) do 206 | buffer[k] = nil -- wipe buffer 207 | end 208 | -- restore output function 209 | write = write_orig 210 | io.write = write_orig 211 | -- turn off output buffering 212 | settings.output_buffering = false 213 | end 214 | end 215 | 216 | function output_get_clean() 217 | local output = tconcat(buffer) 218 | output_clean() 219 | return output 220 | end 221 | 222 | function output_flush() 223 | -- WARNING! need to get output first and then write it, since output function 224 | -- is controlled by output_clean() 225 | local output = output_get_clean() 226 | -- NOTICE! most times this is the first ever call to write(), which takes care 227 | -- of headers, don't use io_write()! 228 | write(output) 229 | end 230 | 231 | function cookie_set(name, value, expires, path, domain) 232 | local cookie_string = ('%s=%s; domain=%s; expires=%s; path=%s'):format( 233 | name, 234 | value, 235 | domain, 236 | date('!%a, %d-%b-%Y %X GMT', expires + time()), 237 | path 238 | ) 239 | 240 | header('Set-Cookie', cookie_string, false) 241 | end 242 | 243 | function cookie_parse() 244 | local parsed, cookies, tmp, key, value = {} 245 | 246 | cookies = explode(';', ophal.raw_cookies) 247 | 248 | for _, v in pairs(cookies) do 249 | v = trim(v) 250 | if #v > 0 then 251 | tmp = explode('=', v) 252 | key = unescape((tmp[1] or ''):gsub('+', ' ')) 253 | value = unescape((tmp[2] or ''):gsub('+', ' ')) 254 | parsed[key] = value 255 | end 256 | end 257 | 258 | return parsed 259 | end 260 | 261 | ophal.cookies = cookie_parse() 262 | -------------------------------------------------------------------------------- /includes/server/nginx.lua: -------------------------------------------------------------------------------- 1 | local seawolf = require 'seawolf'.__build('text') 2 | local time, ngx_print, ngx_var, ngx_req = os.time, ngx.print, ngx.var, ngx.req 3 | local explode, unescape = seawolf.text.explode, socket.url.unescape 4 | local trim = seawolf.text.trim 5 | 6 | env._SERVER = function (v) 7 | if v == 'QUERY_STRING' then 8 | return ngx_var.args 9 | elseif v == 'SCRIPT_NAME' then 10 | return ngx_var.uri 11 | elseif v == 'HTTP_HOST' then 12 | return ngx_req.get_headers()["Host"] 13 | elseif v == 'SERVER_NAME' then 14 | return ngx_var[v:lower()] 15 | else 16 | return ngx_var[v] 17 | end 18 | end 19 | 20 | ophal.raw_cookies = ngx_req.get_headers()['Cookie'] or '' 21 | 22 | function write(s) 23 | ngx.print(s) 24 | end 25 | io.write = write 26 | 27 | function header(n, v) 28 | if n == 'status' then 29 | ngx.status = v 30 | else 31 | if type(v) == 'function' then 32 | v = v() 33 | end 34 | ngx.header[n] = v 35 | end 36 | end 37 | 38 | --[[ 39 | Redirect to raw destination URL. 40 | ]] 41 | function redirect(dest_url, http_response_code) 42 | shutdown_ophal() 43 | ngx.redirect(dest_url, http_response_code or ngx.HTTP_MOVED_TEMPORARILY) 44 | end 45 | 46 | do 47 | local body 48 | function request_get_body() 49 | local file = {} 50 | 51 | if body == nil then 52 | ngx.req.read_body() 53 | -- try from memory 54 | body = ngx.req.get_body_data() 55 | if body == nil then 56 | file.name = ngx.req.get_body_file() 57 | if file.name then 58 | file.handle = io.open(file.name) 59 | body = file.handle:read '*a' 60 | else 61 | body = '' 62 | end 63 | end 64 | end 65 | return body 66 | end 67 | end 68 | 69 | function server_exit() 70 | ngx.exit(ngx.HTTP_OK) 71 | end 72 | -------------------------------------------------------------------------------- /includes/session.lua: -------------------------------------------------------------------------------- 1 | local temp_dir = seawolf.behaviour.temp_dir 2 | local safe_open, safe_write = seawolf.fs.safe_open, seawolf.fs.safe_write 3 | local safe_close, table_dump = seawolf.fs.safe_close, seawolf.contrib.table_dump 4 | local time, base, rawset, tconcat = os.time, base, rawset, table.concat 5 | local format, empty = string.format, seawolf.variable.empty 6 | local session 7 | 8 | -- Session handler 9 | if settings.sessionapi then 10 | if type(settings.sessionapi) ~= 'table' then 11 | settings.sessionapi = {enabled = true} 12 | end 13 | 14 | -- Look for session cookie 15 | local session_id = ophal.cookies['session-id'] or '' 16 | -- if session ID is not valid then set a new ID 17 | if not uuid.isvalid(session_id) then 18 | session_id = uuid.new() 19 | -- Delegate cookie header to ophal.header 20 | cookie_set('session-id', session_id, 3*60*60, base.route, _SERVER 'SERVER_NAME' or '') 21 | end 22 | -- init session table 23 | ophal.session = { 24 | id = session_id, 25 | file = {}, 26 | } 27 | session = ophal.session 28 | end 29 | 30 | function sessions_path() 31 | if settings.sessionapi then 32 | return settings.sessionapi.path or temp_dir() 33 | end 34 | 35 | return temp_dir() 36 | end 37 | 38 | -- Start new or resume existing session 39 | function session_start() 40 | local fh, sign, err, data, data_function, parsed 41 | 42 | if not session.open then 43 | -- Compute session filename 44 | session.file.name = string.format('%s/%s.ophal' , sessions_path(), session.id) 45 | 46 | -- Try to create/read session data 47 | fh, sign, err = safe_open(session.file.name) 48 | 49 | if fh then 50 | session.file.sign = sign 51 | -- Load session data 52 | session.open = true 53 | local data = fh:read('*a') or '' 54 | fh:close() 55 | if data:byte(1) == 27 then 56 | error 'session: binary bytecode in session data!' 57 | end 58 | 59 | -- Parse session data 60 | data_function, err = loadstring(data) 61 | if data_function then 62 | setfenv(data_function, {}) -- empty environment 63 | parsed, data, err = pcall(data_function) 64 | end 65 | if err then 66 | error(format('session: %s', err)) 67 | end 68 | _SESSION = type(data) == 'table' and data or {} 69 | session.data = _SESSION 70 | else 71 | error "session: Can't load session data." 72 | end 73 | end 74 | end 75 | 76 | -- Reset runtime session data 77 | local function session_close() 78 | safe_close(session.file.name, session.file.sign) 79 | session.open = false 80 | _SESSION = nil 81 | end 82 | 83 | -- Write session data and end session 84 | function session_write_close() 85 | local serialized, rawdata, saved, err 86 | 87 | if session.open then 88 | rawdata = {'return '} 89 | serialized, err = pcall(table_dump, session.data, function (s) rawset(rawdata, #rawdata + 1, s) end) 90 | rawdata = tconcat(rawdata) 91 | if serialized then 92 | saved, err = safe_write(session.file.name, session.file.sign, rawdata) 93 | if not saved then 94 | error "session: Can't save session data!" 95 | end 96 | else 97 | error(format('session: %s', err)) 98 | end 99 | session_close() 100 | end 101 | end 102 | 103 | -- Destroys all data registered to a session 104 | function session_destroy() 105 | session_close() 106 | os.remove(session.file.name) 107 | session.data = _SESSION -- global _SESSION is blank ATM 108 | session.id = nil 109 | end 110 | 111 | -- Delete expired sessions 112 | function session_destroy_expired() 113 | local path = sessions_path() 114 | 115 | for file in lfs.dir(path) do 116 | local session_file = file:sub(-6) == '.ophal' 117 | local lock_file = file:sub(-11) == '.ophal.lock' 118 | 119 | if session_file or lock_file then 120 | local filepath = path .. '/' .. file 121 | local attr = lfs.attributes(filepath) 122 | local age = os.difftime(os.time(), attr.change) 123 | 124 | if 125 | (session_file and age > (settings.sessionapi.ttl or 86400)) or 126 | (lock_file and age > (settings.sessionapi.lock_ttl or 120)) 127 | then 128 | os.remove(filepath) 129 | end 130 | 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /includes/theme.lua: -------------------------------------------------------------------------------- 1 | local slash, tinsert, tconcat = settings.slash, table.insert, table.concat 2 | local pcall, settings, empty = pcall, settings, seawolf.variable.empty 3 | local assert, error, setfenv = assert, error, setfenv 4 | local currentdir, xtable = lfs.currentdir() .. slash, seawolf.contrib.seawolf_table 5 | local base, l = base, l 6 | 7 | -- Calculate theme.name 8 | if 9 | settings.mobile and 10 | (mobile.detect.isMobile() or _SERVER 'HTTP_HOST' == settings.mobile.domain_name) 11 | then 12 | theme.name = settings.mobile.theme 13 | else 14 | if type(settings.theme) == 'table' then 15 | theme.name = settings.theme.name 16 | else 17 | theme.name = settings.theme 18 | end 19 | end 20 | 21 | -- Load themes/%/settings.lua 22 | local seawolf = require 'seawolf'.__build('variable', 'contrib') 23 | 24 | if settings.template_env == nil then settings.template_env = {} end 25 | 26 | if settings.theme == nil then settings.theme = {name = 'basic'} end 27 | do 28 | local mt = {} 29 | mt.__index = function(t, k) 30 | if mt[k] ~= nil then 31 | return mt[k] 32 | end 33 | end 34 | mt.override = function(t, vars) 35 | if t.__overrides == nil then 36 | t.__overrides = {} 37 | end 38 | t.__overrides[vars[1]] = vars[2] 39 | end 40 | setmetatable(settings.theme, mt) 41 | end 42 | 43 | if settings.theme.css == nil then settings.theme.css = {} end 44 | setmetatable(settings.theme.css, seawolf.contrib.metahelper) 45 | 46 | if settings.theme.js == nil then settings.theme.js = {} end 47 | setmetatable(settings.theme.js, seawolf.contrib.metahelper) 48 | 49 | local _, settings_builder = pcall(require, ('themes.%s.settings'):format(settings.theme.name)) 50 | if type(settings_builder) == 'function' then 51 | settings_builder(settings.theme, settings.template_env) 52 | end 53 | 54 | -- Set final theme settings 55 | theme.settings = settings.theme 56 | 57 | init_head() 58 | init_css() 59 | init_js() 60 | 61 | function theme_print(v) 62 | if type(v) == 'function' then 63 | v() 64 | else 65 | return print(v) 66 | end 67 | end 68 | 69 | --[[ 70 | Render theme template. 71 | ]] 72 | local function theme_render(f, env) 73 | file = ('%sthemes%s%s%s%s.tpl.html'):format(currentdir, slash, theme.name, slash, f) 74 | 75 | local attr, err = lfs.attributes(file) 76 | if err then 77 | return ("template '%s': %s"):format(file, err) 78 | end 79 | 80 | if attr ~= nil and attr.mode == 'file' then 81 | -- read file contents 82 | local fh = assert(io.open(file)) 83 | local src = ('print [[%s]]'):format(fh:read('*a')) 84 | fh:close() 85 | 86 | -- translate lua template tag 87 | src = src:gsub('(<%?lua)(.-)(%?>)', "]]; %2 print[[") 88 | 89 | -- load source code 90 | local prog, err = loadstring(src, file) 91 | if not prog then 92 | return ("template '%s': %s"):format(file, err) 93 | end 94 | 95 | -- extend env 96 | if not empty(settings.template_env) then 97 | for k, v in pairs(settings.template_env) do 98 | if env[k] == nil then 99 | env[k] = v 100 | end 101 | end 102 | end 103 | 104 | -- jail 105 | local buffer = {} 106 | if env._return then 107 | env.print = function (v) buffer[1 + #buffer] = v end 108 | else 109 | env.print = theme_print 110 | end 111 | env.settings = settings 112 | env.echo = echo 113 | env.base = base 114 | env.theme = theme 115 | env.mobile = mobile 116 | env.print_t = print_t 117 | env.print_f = print_f 118 | env.debug = debug 119 | env.l = l 120 | env.route_arg = route_arg 121 | env.request_path = request_path 122 | env.path_to_theme = path_to_theme 123 | env.pairs = pairs 124 | env.format_date = format_date 125 | env._SERVER = _SERVER 126 | env.mobile = mobile 127 | env.core_version = ophal.version 128 | setfenv(prog, env) 129 | 130 | -- execute 131 | local status, result = pcall(prog) 132 | if status then 133 | return tconcat(buffer) 134 | else 135 | return ("template '%s': %s"):format(file, result) 136 | end 137 | end 138 | end 139 | 140 | --[[ 141 | Execute theme function. 142 | ]] 143 | local function theme_execute(f, arg) 144 | local status, result = pcall(theme[f], arg) 145 | if status then 146 | if type(result) == 'function' then 147 | status, result = pcall(result) 148 | if status then 149 | return result 150 | end 151 | else 152 | return result 153 | end 154 | end 155 | return ("theme function %s: '%s'"):format(f, result) 156 | end 157 | 158 | --[[ 159 | Call theme function 160 | ]] 161 | local function theme_call(t, arg) 162 | if not arg then arg = {} end 163 | 164 | local f = arg[1] 165 | 166 | arg[1] = nil -- clean-up theme environment 167 | 168 | -- Let modules alter theme function arguments 169 | module_invoke_all('theme_preprocess', f, arg) 170 | 171 | if t[f] == nil then 172 | return theme_render(f, arg) 173 | else 174 | return theme_execute(f, arg) 175 | end 176 | end 177 | 178 | --[[ 179 | Theme metatable 180 | ]] 181 | local mt = { 182 | __call = function(t, arg) 183 | local meta = getmetatable(t) 184 | local overrides = settings.theme.__overrides or {} 185 | 186 | -- Override theme functions 187 | for k in pairs(overrides) do 188 | if (overrides and type(overrides) == 'table') then 189 | local orig = t[k] 190 | t[k] = function(...) 191 | return overrides[k](..., orig, theme) 192 | end 193 | end 194 | end 195 | 196 | meta.__call = theme_call 197 | return meta.__call(t, arg) 198 | end, 199 | } 200 | 201 | setmetatable(theme, mt) 202 | 203 | --[[ 204 | Translate given table key-value pairs to attr="value". 205 | ]] 206 | function render_attributes(options, default_options) 207 | if default_options == nil then default_options = {} end 208 | 209 | -- Merge default_options into options 210 | if type(options) ~= 'table' then 211 | options = default_options 212 | else 213 | for k, v in pairs(default_options) do 214 | if options[k] == nil then 215 | options[k] = default_options[k] 216 | end 217 | end 218 | end 219 | 220 | local attr = {} 221 | 222 | for k, v in pairs(options) do 223 | tinsert(attr, ('%s="%s"'):format(k, v)) 224 | end 225 | return tconcat(attr, " ") 226 | end 227 | 228 | --[[ 229 | Translate given table key-value pairs to "val1 val2". 230 | ]] 231 | function render_classes(classes, default_classes) 232 | if default_classes == nil then default_classes = {} end 233 | 234 | if type(classes) ~= 'table' then 235 | classes = default_classes 236 | else 237 | for k, v in pairs(default_classes) do 238 | if classes[k] == nil and not empty(classes[k]) then 239 | classes[k] = default_classes[k] 240 | end 241 | end 242 | end 243 | 244 | local output = {} 245 | 246 | for k, _ in pairs(classes) do 247 | tinsert(output, k) 248 | end 249 | return tconcat(output, '') 250 | end 251 | 252 | --[[ 253 | Print output of given theme function and parameters. 254 | ]] 255 | function print_t(...) 256 | print(theme(...) or '') 257 | end 258 | 259 | --[[ 260 | Print output of given theme function and parameters. 261 | ]] 262 | function print_f(text, ...) 263 | print(text:format(...)) 264 | end 265 | 266 | --[[ 267 | Return the output of given theme function and parameters. 268 | ]] 269 | function render_t(arg) 270 | arg._return = true 271 | return theme(arg) 272 | end 273 | 274 | function theme.json(variables) 275 | local json = require 'dkjson' 276 | local content = variables.content 277 | 278 | if not variables.status then 279 | content = {error = content} 280 | end 281 | 282 | local output = json.encode(content) 283 | 284 | header('content-type', 'application/json; charset=utf-8') 285 | header('content-length', (output or ''):len()) 286 | 287 | theme_print(output) 288 | end 289 | 290 | function path_to_theme() 291 | return ('themes/%s'):format(theme.name) 292 | end 293 | 294 | function theme_blocks_load() 295 | module_invoke_all('blocks_alter', ophal.blocks) 296 | end 297 | 298 | function theme_regions_load() 299 | ophal.regions = module_invoke_all('region') 300 | 301 | -- Default regions 302 | ophal.regions.sidebar_first = { 303 | id = 'sidebar_first', 304 | blocks = xtable(), 305 | } 306 | ophal.regions.sidebar_last = { 307 | id = 'sidebar_last', 308 | blocks = xtable(), 309 | } 310 | 311 | module_invoke_all('regions_alter', ophal.regions) 312 | end 313 | 314 | function theme_get_regions() 315 | local output = {} 316 | 317 | for _, region in pairs(ophal.regions) do 318 | if not empty(region) then 319 | local region_output = {} 320 | for _, block in pairs(region.blocks) do 321 | if block.id and not empty(ophal.blocks[block.id]) then 322 | block = ophal.blocks[block.id] 323 | region_output[#region_output + 1] = function () 324 | theme{'block', entity = block} 325 | end 326 | end 327 | end 328 | 329 | -- Delayed rendering, this function will be called on theme render 330 | output[region.id] = function () 331 | for k, v in pairs(region_output) do 332 | v() 333 | end 334 | end 335 | end 336 | end 337 | 338 | return output 339 | end 340 | 341 | --[[ 342 | Anchor theme function. 343 | ]] 344 | function theme.a(variables) 345 | if variables == nil then variables = {} end 346 | 347 | local attributes = variables.attributes 348 | variables.attributes = nil 349 | 350 | -- Support HTML5 download attribute 351 | local download = attributes.download 352 | attributes.download = nil 353 | if download == true then 354 | download = ' download' 355 | elseif type(download) == 'string' then 356 | download = (' download="%s"'):format(download) 357 | end 358 | 359 | return ('%s'):format(variables.route, download or '', render_attributes(attributes), variables.text) 360 | end 361 | 362 | --[[ 363 | Image theme function. 364 | ]] 365 | function theme.img(variables) 366 | local path = variables.path or '' 367 | local options = variables.options 368 | 369 | if options and options.external then 370 | options.external = nil 371 | else 372 | path = base.route .. path 373 | end 374 | return (''):format(path, render_attributes(options)) 375 | end 376 | 377 | --[[ 378 | Logo theme function. 379 | ]] 380 | function theme.logo() 381 | local site = settings.site 382 | local logo_path = ('%s/%s'):format(path_to_theme(), site.logo_path) 383 | return l(theme{'img', path = logo_path, options = {alt = site.logo_title, title = site.logo_title, border = 0}}, '', {absolute = true, attributes = {id = 'logo'}}) 384 | end 385 | 386 | --[[ 387 | Items list function theme function. 388 | ]] 389 | function theme.item_list(variables) 390 | if variables == nil then variables = {} end 391 | 392 | local list = variables.list 393 | variables.list = nil 394 | 395 | local output = {(''):format(list ~= nil and ' ' .. render_attributes(variables) or '')} 396 | for _, v in pairs(list) do 397 | tinsert(output, ('
  • %s
  • '):format(v)) 398 | end 399 | tinsert(output, '') 400 | return tconcat(output) 401 | end 402 | -------------------------------------------------------------------------------- /index.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua5.1 2 | 3 | require 'includes.bootstrap' 4 | 5 | ophal.bootstrap(nil, function () 6 | route_execute_active_handler() 7 | end) 8 | -------------------------------------------------------------------------------- /install.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua5.1 2 | 3 | require 'includes.bootstrap' 4 | 5 | settings.output_buffering = false 6 | 7 | ophal.bootstrap(5, function () 8 | -- Settings 9 | local default_settings = { 10 | site = { 11 | name = 'Ophal', 12 | logo_title = 'The Ophal Project', 13 | logo_path = 'images/ophalproject.png', 14 | }, 15 | slash = string.sub(package.config,1,1), 16 | language = 'en', 17 | language_dir = 'ltr', 18 | } 19 | 20 | for k, v in pairs(default_settings) do 21 | if settings[k] == nil then 22 | settings[k] = v 23 | end 24 | end 25 | 26 | -- Force settings 27 | settings.theme = {name = 'install'} 28 | 29 | -- Detect phase 30 | local phase = tonumber(_GET.phase) or 1 31 | 32 | -- Load Core API 33 | require 'includes.common' 34 | require 'includes.locale' 35 | require 'includes.route' 36 | require 'includes.theme' 37 | 38 | -- Pager 39 | theme.install_pager = function (variables) 40 | if variables == nil then variables = {} end 41 | 42 | local options = variables.options 43 | local previous = '' 44 | 45 | if phase > 1 then 46 | previous = ('

    << Previous '):format(base.system_root , base.route, phase - 1) 47 | end 48 | 49 | return table.concat{ 50 | ('

    '):format(render_attributes(options)), 51 | previous, 52 | ('Next >>

    '):format(base.system_root, base.route, phase + 1), 53 | '
    ' 54 | } 55 | end 56 | 57 | -- phases 58 | local content = '' 59 | local phases = { 60 | -- Welcome page 61 | function () 62 | -- Look for settings.lua 63 | if seawolf.fs.is_file [[settings.lua]] then 64 | -- Redirect to next phase 65 | redirect(('%s%sinstall.cgi?phase=3'):format(base.system_root, base.route)) 66 | end 67 | 68 | page_set_title 'Phase 1: Welcome!' 69 | 70 | content = ([[

    Welcome to the Ophal installation process.

    71 |

    Before you proceed, please consider the following

    72 |

      73 |
    1. For enhanced security, do *not* run this installer in production.
    2. 74 |
    3. Javascript enabled is needed in order to complete the installation.
    4. 75 |
    5. No dabatase is created by this installer, you need to create one in advance.
    6. 76 |
    77 |

    78 | ]] .. theme{'install_pager'}):format(base.system_root, base.route, 2) 79 | end, 80 | 81 | -- Verify pre-requisites 82 | function () 83 | local libraries, status, err, output, continue 84 | local tinsert, tconcat = table.insert, table.concat 85 | 86 | -- Look for settings.lua 87 | if seawolf.fs.is_file 'settings.lua' then 88 | -- Redirect to next phase 89 | redirect(('%s%sinstall.cgi?phase=3'):format(base.system_root, base.route)) 90 | end 91 | 92 | page_set_title 'Phase 2: Pre-requisites' 93 | 94 | -- Library checker 95 | libraries = { 96 | ['socket.url'] = { 97 | name = 'LuaSocket', 98 | required = true, 99 | }, 100 | lfs = { 101 | name = 'LuaFilesystem', 102 | required = true, 103 | }, 104 | uuid = { 105 | name = 'luuid', 106 | required = true, 107 | }, 108 | DBI = { 109 | name = 'LuaDBI', 110 | required = true, 111 | }, 112 | lpeg = { 113 | name = 'LPEG', 114 | required = true, 115 | }, 116 | dkjson = { 117 | name = "David Kolf's JSON", 118 | required = true, 119 | }, 120 | ['seawolf.variable'] = { 121 | name = 'Seawolf: variable', 122 | required = true, 123 | }, 124 | ['seawolf.fs'] = { 125 | name = 'Seawolf: filesystem', 126 | required = true, 127 | }, 128 | ['seawolf.text'] = { 129 | name = 'Seawolf: text', 130 | required = true, 131 | }, 132 | ['seawolf.behaviour'] = { 133 | name = 'Seawolf: behaviour', 134 | required = true, 135 | }, 136 | ['seawolf.contrib'] = { 137 | name = 'Seawolf: contrib', 138 | required = true, 139 | }, 140 | } 141 | output = { 142 | '', 143 | '' 144 | } 145 | 146 | -- Find required libraries, both optional and required 147 | continue = true 148 | for machine_name, library in pairs(libraries) do 149 | tinsert(output, '') 150 | tinsert(output, (''):format(library.name)) 151 | tinsert(output, (''):format(machine_name)) 152 | tinsert(output, (''):format(library.required and 'Required' or 'Optional')) 153 | -- Status 154 | status, err = pcall(require, machine_name) 155 | if status then 156 | tinsert(output, '') 157 | else 158 | continue = false 159 | tinsert(output, (''):format(err)) 160 | end 161 | tinsert(output, '') 162 | end 163 | tinsert(output, '
    LibraryMachine nameRequired?StatusError
    %s"%s"%sFoundMissing
    "%s"
    ') 164 | -- Say: All requirements are OK 165 | if continue then 166 | tinsert(output, theme{'install_pager'}) 167 | else 168 | tinsert(output, '

    Please install any missing library. Read the documentation for details.

    ') 169 | end 170 | content = tconcat(output) 171 | end, 172 | 173 | -- Generate configuration file 174 | function () 175 | local tinsert, tconcat = table.insert, table.concat 176 | 177 | -- Look for settings.lua 178 | if seawolf.fs.is_file 'settings.lua' then 179 | -- Redirect to next phase 180 | redirect(('%s%sinstall.cgi?phase=4'):format(base.system_root, base.route)) 181 | end 182 | 183 | require 'includes.module' 184 | require 'includes.form' 185 | 186 | add_js 'libraries/jquery.min.js' 187 | add_js 'libraries/uuid.js' 188 | add_js{[[ 189 | $(document).ready(function() { 190 | $('#generate').click(function() { 191 | $('#settings').html($('#settings_template').html() 192 | .replace('!site_name', $('#sitename').val()) 193 | .replace('!files_path', $('#files_path').val()) 194 | .replace('!lorem_ipsum_module', $('#lorem_ipsum_module').is(':checked')) 195 | .replace('!content_module', $('#content_module').is(':checked')) 196 | .replace('!comment_module', $('#comment_module').is(':checked')) 197 | .replace('!user_module', $('#user_module').is(':checked')) 198 | .replace('!tag_module', $('#tag_module').is(':checked')) 199 | .replace('!menu_module', $('#menu_module').is(':checked')) 200 | .replace('!file_module', $('#file_module').is(':checked')) 201 | .replace('!boost_module', $('#boost_module').is(':checked')) 202 | .replace('!test_module', $('#test_module').is(':checked')) 203 | ); 204 | $('#vault').html($('#vault_template').html() 205 | .replace('!site_hash', uuid()) 206 | .replace('!db_filepath', $('#db_filepath').val()) 207 | ); 208 | $('#install_pager').show(); 209 | }); 210 | }); 211 | ]], type = 'inline'} 212 | 213 | page_set_title 'Phase 3: Configuration file settings.lua' 214 | 215 | content = tconcat{ 216 | '

    Step 1. Configure your site

    ', 217 | theme{'form', action = 'install.cgi', 218 | elements = { 219 | {'textfield', title = 'Site name', value = 'Ophal', attributes = {id = 'sitename'}, weight = 1}, 220 | {'textfield', title = 'Database file path', attributes = {id = 'db_filepath'}, weight = 5}, 221 | {'textfield', title = 'File directory', value = 'files', attributes = {id = 'files_path'}, weight = 10}, 222 | {'checkbox', title = 'Enable the Lorem Ipsum module', value = '1', attributes = {id = 'lorem_ipsum_module'}, weight = 15}, 223 | {'checkbox', title = 'Enable the Content module', value = '0', attributes = {id = 'content_module'}, weight = 20}, 224 | {'checkbox', title = 'Enable the Comment module', value = '0', attributes = {id = 'comment_module'}, weight = 25}, 225 | {'checkbox', title = 'Enable the User module', value = '0', attributes = {id = 'user_module'}, weight = 30}, 226 | {'checkbox', title = 'Enable the Tag module', value = '0', attributes = {id = 'tag_module'}, weight = 35}, 227 | {'checkbox', title = 'Enable the Menu module', value = '0', attributes = {id = 'menu_module'}, weight = 40}, 228 | {'checkbox', title = 'Enable the File module (experimental)', value = '0', attributes = {id = 'file_module'}, weight = 45}, 229 | {'checkbox', title = 'Enable the Boost module (experimental)', value = '0', attributes = {id = 'boost_module'}, weight = 50}, 230 | {'checkbox', title = 'Enable the Test module (experimental)', value = '0', attributes = {id = 'test_module'}, weight = 55}, 231 | {'button', value = 'Generate', attributes = {id = 'generate'}, weight = 100}, 232 | } 233 | }, 234 | '
    ', 235 | [=[]=], 368 | '
    ', 369 | [=[]=], 410 | theme{'install_pager', style = 'display: none;'} 411 | } 412 | end, 413 | 414 | -- Do install 415 | function () 416 | local status, err, file_directory, fh 417 | local tconcat = table.concat 418 | local output = '' 419 | 420 | -- Load settings 421 | local status, err = pcall(require, 'settings') 422 | 423 | if not status then 424 | err = "Missing file or error when trying to load 'settings.lua'" 425 | else 426 | err = nil 427 | 428 | -- Check 'files' directory permissions 429 | if not (settings.site and settings.site.files_path) then 430 | err = 'settings.site.files_path is not set!' 431 | else 432 | files_path = settings.site.files_path 433 | 434 | if seawolf.fs.is_file(files_path) then 435 | err = ("Created file directory: '%s' is an actual file, not a directory! Please fix and try again."):format(files_path) 436 | elseif seawolf.fs.is_dir(files_path) then 437 | if not seawolf.fs.is_writable(files_path) then 438 | err = ("File directory: '%s' is not writable!"):format(files_path) 439 | else 440 | fh = io.open(files_path .. '/.htaccess', 'w') 441 | fh:write([[SetHandler Ophal_Security_Do_Not_Remove 442 | Options None 443 | Options +FollowSymLinks 444 | 445 | ]]) 446 | output = 447 | '

    Installation complete!

    ' .. 448 | ('

    Your new site is available here

    '):format(base.route) 449 | fh:close() 450 | end 451 | else 452 | err = ("File directory not found! Please create directory '%s'."):format(files_path) 453 | end 454 | end 455 | end 456 | 457 | page_set_title 'Installing...' 458 | content = tconcat{ 459 | err and ('Error: %s'):format(err) or '', 460 | output, 461 | } 462 | end 463 | } 464 | 465 | -- Run phase 466 | phases[phase]() 467 | 468 | -- Render page 469 | print_t{'html', 470 | header_title = ophal.header_title, 471 | title = ophal.title, 472 | content = content, 473 | javascript = get_js(), 474 | css = get_css(), 475 | } 476 | end) 477 | -------------------------------------------------------------------------------- /libraries/ophal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ophal jQuery library. 3 | */ 4 | 5 | (function($) {window.Ophal = new function(namespace, func) { 6 | 7 | this.settings = {}; 8 | 9 | this.set_message = function(message) { 10 | var message = $('
    ' + message + '
    '); 11 | $(message).click(function () { 12 | if (confirm('Do you wish to hide this message?')) { 13 | $(this).remove(); 14 | } 15 | }); 16 | $('#messages').append(message); 17 | }; 18 | 19 | this.extend = function (namespace, func) { 20 | (this[namespace] = func)($); 21 | }; 22 | 23 | this.scroll_down = function() { 24 | if (window.location.href.split('#')[1]) { 25 | window.location = window.location; 26 | } 27 | }; 28 | 29 | this.post = function(config) { 30 | return $.ajax({ 31 | type: 'POST', 32 | url: this.settings.core.base.route + config.url, 33 | contentType: 'application/json; charset=utf-8', 34 | data: JSON.stringify(config.data), 35 | dataType: 'json', 36 | processData: false, 37 | success: config.success, 38 | error: config.error 39 | }); 40 | }; 41 | 42 | this.progress = function(selector, value) { 43 | $(selector + ' .progress .meter').css('width', value + '%'); 44 | }; 45 | 46 | this.t = function(value) { 47 | if ('locale' in this.settings && this.settings.locale[value]) { 48 | return this.settings.locale[value]; 49 | } 50 | 51 | return value; 52 | } 53 | 54 | }})(jQuery); 55 | -------------------------------------------------------------------------------- /libraries/uuid.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * DO WTF YOU WANT TO PUBLIC LICENSE 3 | * Version 2, December 2004 4 | * 5 | * Copyright (C) 2011 Alexey Silin 6 | * 7 | * https://gist.github.com/LeverOne/1308368 8 | * 9 | * Everyone is permitted to copy and distribute verbatim or modified 10 | * copies of this license document, and changing it is allowed as long 11 | * as the name is changed. 12 | * 13 | * DO WTF YOU WANT TO PUBLIC LICENSE 14 | * TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 15 | * 16 | * 0. You just DO WTF YOU WANT TO. 17 | */ 18 | (function(a){a.uuid=function(a,b){for(b=a='';a++<36;b+=a*51&52?(a^15?8^Math.random()*(a^20?16:4):4).toString(16):'-');return b}})(window) -------------------------------------------------------------------------------- /lighttpd.ophal.lua: -------------------------------------------------------------------------------- 1 | -- $Id$ 2 | 3 | --[[ 4 | 5 | develCuy's Ultimate Lua Script for running Ophal on Lighty! 6 | 7 | Thanks to Garret Albright's script for Drupal. 8 | 9 | This script handles URL rewriting, "www." addition/removal, and Boost support 10 | for Ophal installations running on the lighttpd web server daemon. These are 11 | tasks which would be handled by ".htaccess" files when running on the Apache 12 | server. 13 | 14 | To use this script, make the necessary modifications in the "Configuration" 15 | section below to suit your needs and save it as ophal.lua somewhere on your 16 | server where the Lighty daemon will be able to read it. Then, add the following 17 | two lines to your lighttpd.conf file (modifying the second line as necessary): 18 | 19 | server.modules += ("mod_magnet") 20 | magnet.attract-physical-path-to = ("/path/to/ophal.lua") 21 | 22 | Of course, your mileage may vary depending on your existing configuration, but 23 | that's the basics. For more information about mod_magnet and using Lua scripts 24 | with Lighty, see this page in Lighty's documentation wiki: 25 | http://redmine.lighttpd.net/projects/lighttpd/wiki/Docs:ModMagnet 26 | 27 | For more information on the Lua scripting language itself, see: 28 | http://lua-users.org/wiki/LearningLua 29 | http://www.lua.org/manual/5.1/ 30 | 31 | --]] 32 | 33 | -- Configuration: 34 | -- "www" addition/removal: If you want this script to ADD the "www." prefix 35 | -- to the host name when requests are made to the server without it, set the 36 | -- "love_www" value to "true" (without quotes). This will cause requests for 37 | -- "http://example.com/" to be redirected to "http://www.example.com/". If 38 | -- you want it to REMOVE the prefix when requests are made to the server WITH 39 | -- it, set the value to "false"; "http://www.example.com/" will be redirected 40 | -- to "http://example.com/". If you don't want to do any redirection either 41 | -- way, set the value to "nil". 42 | 43 | love_www = false 44 | 45 | -- Subdirectory: If your Ophal installation is in a subdirectory - for 46 | -- example, at http://example.com/ophal/ instead of just 47 | -- http://example.com/ - set the d_path variable below to the path to that 48 | -- directory, with a slash at the beginning AND at the ending of the path. For 49 | -- the example above, you would set d_path to '/ophal/' (with quotes). If 50 | -- your Ophal installation is at your web site's root - for example, just at 51 | -- http://example.com/ - set this variable to a single slash: '/' 52 | 53 | d_path = '/' 54 | 55 | -- Boost support: Set to "true" (without quotes) to enable rewriting necessary 56 | -- for the Boost module. Set to "false" if you're not using Boost. If you are 57 | -- using Boost, it's strongly recommended that you also enable "www." addition 58 | -- or removal above (set love_www to true or false); otherwise, Boost will 59 | -- create duplicate copies of its cache files. 60 | 61 | boost_on = false 62 | 63 | -- Boost path header: If set to "true", Boost will add a "X-Boost-Path" HTTP 64 | -- header to Boost cache-worthy requests which either contains the path to 65 | -- the cache file in the case of a cache hit, or the word "miss" in the case 66 | -- of a cache miss. This may be handy when initially setting up or debugging 67 | -- Boost on your server, but you may wish to turn it off (set to "false") for 68 | -- live sites to save a little overhead and possibly for security reasons. 69 | 70 | boost_header = false 71 | 72 | -- Simple anti-leech protection: If set to "false", anti-leech protection won't 73 | -- kick in. Otherwise, set it to a table (like an array) of file extensions 74 | -- which should be protected. See the commented-out line below for a basic 75 | -- example. Check out this documentation for Lua's simple pattern matching 76 | -- syntax: http://www.lua.org/manual/5.1/manual.html#5.4.1 77 | 78 | anti_leech = false 79 | 80 | -- anti_leech = { 'jpe?g', 'png', 'gif', 'mov', 'avi', 'wmv', 'mpe?g', 'mp[234]', 'wav', 'wma', 'swf', 'flv' } 81 | 82 | -- ---- Stop! You should not need to edit anything else below this line. ---- -- 83 | 84 | -- Remove "www" from URLs. Note that unlike the .htaccess file that comes with 85 | -- Ophal, you don't have to edit this to add your site's/sites' URL/URLs - we 86 | -- can determine that automatically. 87 | -- Match "www." at the beginning of the URL. 88 | -- Note that Lua's matching system is inspired by standard regular 89 | -- expressions, but is not a drop-in replacement. In this case it's close 90 | -- enough, though. See: http://www.lua.org/manual/5.1/manual.html#5.4.1 91 | -- The match function returns nil when there's no match. 92 | 93 | if love_www == false and lighty.env['uri.authority']:match('^www%.') ~= nil then 94 | -- Rebuild the URL without the "www." and pass it as the "Location" header. 95 | -- Note that Lua's string counting functions are 1-based (the first character 96 | -- is at position 1, not position 0 as in most other languages), so the 5 97 | -- parameter is correct for the sub() function below. 98 | lighty.header['Location'] = lighty.env['uri.scheme'] .. '://' .. lighty.env['uri.authority']:sub(5) .. lighty.env['request.orig-uri'] 99 | -- Return a 301 Moved Permanently HTTP status code. 100 | return 301 101 | end 102 | 103 | -- Add "www" to URLs. Read the comments in the "Remove 'www'" section above for 104 | -- more info - much of it could be repeated here. 105 | 106 | if love_www and lighty.env['uri.authority']:match('^www%.') == nil then 107 | -- Rebuild URL, adding "www.", and pass it in as the "Location" header. 108 | lighty.header['Location'] = lighty.env['uri.scheme'] .. '://www.' .. lighty.env['uri.authority'] .. lighty.env['request.orig-uri'] 109 | return 301 110 | end 111 | 112 | -- We don't want directories (such as the root document directory when '/' is 113 | -- requested) to be counted as a "file" just because it will respond to 114 | -- lighty.stat() with something other than nil. This bit of ugliness lets us do 115 | -- so without creating another variable to store the results of lighty.stat() 116 | -- and then doing file_exists = stat ~= nil and stat.is_file 117 | local file_exists = lighty.env['physical.path']:sub(-1) ~= '/' and lighty.stat(lighty.env['physical.path']) ~= nil 118 | local path_trimmed = lighty.env['uri.path']:sub(d_path:len() + 1) 119 | 120 | -- Anti-leeching 121 | if anti_leech ~= false and file_exists and lighty.request['Referer'] ~= nil then 122 | for idx, ext in ipairs(anti_leech) do 123 | if path_trimmed:match('%.' .. ext .. '$') then 124 | -- This extension is in the blacklist. Is the visitor leeching? 125 | -- Not using pattern matching here because there doesn't seem to be an 126 | -- easy way to escape reserved characters in a pattern. Dashes are the 127 | -- biggest problem in that regard. 128 | accessing = lighty.env['uri.scheme'] .. '://' .. lighty.env['uri.authority'] 129 | if (lighty.request['Referer']:sub(1,accessing:len()) == accessing) then 130 | return 131 | else 132 | -- Return a 403 Forbidden HTTP status code. 133 | return 403 134 | end 135 | end 136 | end 137 | end 138 | 139 | if boost_on then 140 | if file_exists then 141 | -- If the file exists, only try to Boost JS and CSS files (not images or 142 | -- anything else). This naively assumes all CSS and JS requests will use 143 | -- their normal extensions, but maybe Boost is using the same assumption…? 144 | ext = path_trimmed:match('%.%a+') 145 | if ext ~= '.js' and ext ~= '.css' then 146 | return 147 | end 148 | end 149 | --[[ 150 | Check for the existence of files at physical paths. 151 | @param paths 152 | A table of path info to check, keyed by physical path. 153 | @return 154 | true if a file exists at a path in the table; false otherwise. 155 | --]] 156 | local check_exists = function(paths) 157 | for idx, stats in ipairs(paths) do 158 | if lighty.stat(stats.physical) then 159 | lighty.env['physical.path'] = stats.physical 160 | lighty.env['uri.path'] = stats.path 161 | lighty.env['physical.rel-path'] = stats.path 162 | lighty.header['Content-Type'] = stats.ctype 163 | if stats.gzip then 164 | lighty.header['Content-Encoding'] = 'gzip' 165 | end 166 | if boost_header then 167 | lighty.header['X-Boost-Path'] = stats.path 168 | end 169 | return true 170 | end 171 | end 172 | return false 173 | end 174 | 175 | -- Make sure there's something in the Cookie value to avoid having to check 176 | -- against nil more than once 177 | if (lighty.request['Cookie'] == nil) then 178 | lighty.request['Cookie'] = '' 179 | end 180 | 181 | local gzip_on = (lighty.request['Accept-Encoding'] ~= nil and lighty.request['Accept-Encoding']:find('gzip', 1, true)) or lighty.request['Cookie']:find('boost-gzip', 1, true) 182 | 183 | -- cache/perm files might exist in their non-cache location (in which case, 184 | -- file_exists == true at this point), but even in that case we want to serve 185 | -- them from the cache directory anyway (for ghetto Gzip support, for 186 | -- example). 187 | local perm = {} 188 | 189 | if path_trimmed == 'boost-gzip-cookie-test.html' then 190 | -- For whatever reason, OOP-style function calling isn't working on these 191 | -- tables (perm:insert(item) causes the error "attempt to call method 192 | -- 'insert' (a nil value)"), so we do it functional-style. 193 | table.insert(perm, { 194 | ['physical'] = lighty.env['physical.doc-root'] .. d_path .. 'cache/perm/boost-gzip-cookie-test.html.gz', 195 | ['ctype'] = 'text/html', 196 | ['path'] = d_path .. 'cache/perm/boost-gzip-cookie-test.html.gz', 197 | ['gzip'] = true, 198 | }) 199 | elseif ext ~= nil then 200 | local path = d_path .. 'cache/perm/' .. lighty.env['uri.authority'] .. '/' .. path_trimmed .. '_' .. ext 201 | local physical = lighty.env['physical.doc-root'] .. path 202 | local types = { 203 | ['.css'] = 'text/css', 204 | ['.js'] = 'text/javascript', 205 | } 206 | if gzip_on then 207 | table.insert(perm, { 208 | ['physical'] = physical .. '.gz', 209 | ['ctype'] = types[ext], 210 | ['path'] = path .. '.gz', 211 | ['gzip'] = true, 212 | }) 213 | end 214 | table.insert(perm, { 215 | ['physical'] = physical, 216 | ['ctype'] = types[ext], 217 | ['path'] = path, 218 | ['gzip'] = false, 219 | }) 220 | end 221 | 222 | boost_hit = #perm ~= 0 and check_exists(perm) 223 | 224 | -- If no hits yet, and we might have a hit in cache/normal… 225 | if not boost_hit then 226 | if file_exists then 227 | -- Just serve the file! 228 | return 229 | end 230 | -- Patterns for paths Boost doesn't cache. Lua's patterns lack an "or" 231 | -- operator like the pipe in regular expressions, so instead of something 232 | -- like '^(admin|cache|etc)' we have this kludge. 233 | for idx, path in ipairs({ 234 | '^admin', 235 | '^cache', 236 | '^misc', 237 | '^modules', 238 | '^sites', 239 | '^system', 240 | '^openid', 241 | '^themes', 242 | '^node/add', 243 | '^comment/reply', 244 | '^edit', 245 | '^user$', 246 | '^user/[^%d]', 247 | }) do 248 | if path_trimmed:match(path) then 249 | bad_path = true 250 | break 251 | end 252 | end 253 | if not bad_path == true and lighty.env['request.method'] == 'GET' and lighty.env['uri.scheme'] ~= 'https' and lighty.request['Cookie']:find('DRUPAL_UID', 1, true) == nil then 254 | local path = d_path .. 'cache/normal/' .. lighty.env['uri.authority'] .. '/' .. path_trimmed .. '_' 255 | if lighty.env['uri.query'] ~= nil then 256 | path = path .. lighty.env['uri.query'] 257 | end 258 | local physical = lighty.env['physical.doc-root'] .. path 259 | local types = { 260 | { 261 | ['ext'] = '.html', 262 | ['ctype'] = 'text/html', 263 | }, 264 | { 265 | ['ext'] = '.xml', 266 | ['ctype'] = 'text/xml', 267 | }, 268 | { 269 | ['ext'] = '.json', 270 | ['ctype'] = 'text/javascript', 271 | }, 272 | } 273 | local norm = {} 274 | 275 | if gzip_on then 276 | -- Similarly to above, types:ipairs() does not work 277 | for idx, type in ipairs(types) do 278 | table.insert(norm, { 279 | ['physical'] = physical .. type.ext .. '.gz', 280 | ['ctype'] = type.ctype, 281 | ['path'] = path .. type.ext .. '.gz', 282 | ['gzip'] = true, 283 | }) 284 | end 285 | end 286 | for idx, type in ipairs(types) do 287 | table.insert(norm, { 288 | ['physical'] = physical .. type.ext, 289 | ['ctype'] = type.ctype, 290 | ['path'] = path .. type.ext, 291 | ['gzip'] = false, 292 | }) 293 | end 294 | 295 | -- Check the norm for hits 296 | boost_hit = check_exists(norm) 297 | end 298 | end 299 | if boost_header and not boost_hit then 300 | lighty.header['X-Boost-Path'] = 'miss' 301 | end 302 | end 303 | 304 | if not file_exists and (not boost_on or not boost_hit) then 305 | -- Rewrite the query part of the URI (or create it if there isn't one) to 306 | -- append "q=" (while stripping away the path to the Ophal installation 307 | -- if it's in there). 308 | if lighty.env['uri.query'] == nil or lighty.env['uri.query']:match('^q=') == nil then 309 | lighty.env['uri.query'] = (lighty.env['uri.query'] == nil and '' or lighty.env['uri.query'] .. '&') .. 'q=' .. path_trimmed 310 | end 311 | lighty.env['uri.path'] = d_path .. 'index.cgi' 312 | lighty.env['physical.rel-path'] = lighty.env['uri.path'] 313 | lighty.env['physical.path'] = lighty.env['physical.doc-root'] .. lighty.env['physical.rel-path'] 314 | end 315 | -------------------------------------------------------------------------------- /modules/boost/init.lua: -------------------------------------------------------------------------------- 1 | local print, echo, settings, request_path = print, echo, settings, request_path 2 | local output_clean, output_get_clean = output_clean, output_get_clean 3 | local io, os, fs, lfs = io, os, seawolf.fs, require 'lfs' 4 | local print_r, require = seawolf.variable.print_r, require 5 | local _SESSION, session_write_close = _SESSION, session_write_close 6 | local exit_ophal = exit_ophal 7 | 8 | module 'ophal.modules.boost' 9 | 10 | --[[ Return cache file path for current page. 11 | ]] 12 | local function filepath() 13 | return ('%s%s.html'):format(settings.boost.path, request_path():gsub([[/]], '_'):gsub([[\.]], '_')) 14 | end 15 | 16 | --[[ Implementation of hook boot() 17 | ]] 18 | function boot() 19 | local file 20 | 21 | if settings.output_buffering then 22 | file = filepath() 23 | if fs.is_file(file) and not has_expired(file) then 24 | output_clean() 25 | if _SESSION then 26 | session_write_close() 27 | end 28 | io.input(file) 29 | print(io.read('*all')) 30 | exit_ophal() 31 | os.exit() 32 | end 33 | end 34 | end 35 | 36 | --[[ Given a timestamp, return formatted date by Boost format date. 37 | ]] 38 | function format_date(ts) 39 | return os.date(settings.boost.date_format, ts) 40 | end 41 | 42 | --[[ Given a timestamp, return cache signature. 43 | ]] 44 | function signature() 45 | local ts = os.time() 46 | local created = format_date(ts) 47 | local expires = format_date(ts + settings.boost.lifetime) 48 | return (settings.boost.signature):format(created, expires) 49 | end 50 | 51 | --[[ Given a file path, return cache expiration status. 52 | ]] 53 | function has_expired(file) 54 | local ts = lfs.attributes(file, 'modification') 55 | 56 | if ts + settings.boost.lifetime <= os.time() then 57 | os.remove(file) 58 | return true 59 | end 60 | end 61 | 62 | --[[ Implementation of hook exit(). 63 | ]] 64 | function exit() 65 | local file, output, fh 66 | 67 | if settings.output_buffering then 68 | file = filepath() 69 | if not fs.is_file(file) then 70 | lfs.mkdir(settings.boost.path) -- force create file cache directory 71 | output = output_get_clean() 72 | if fs.is_dir(settings.boost.path) then 73 | -- Store output to cache 74 | fh = io.open(file, 'w+') 75 | fh:write(output) 76 | -- Append signature 77 | if settings.boost.signature ~= nil then 78 | fh:write(signature()) 79 | end 80 | fh:close() 81 | end 82 | -- Display output, wether it is in cache or not. 83 | print(output) 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /modules/comment/comment.js: -------------------------------------------------------------------------------- 1 | Ophal.extend('comment', function ($) { 2 | 3 | var renderHandlers = {}; 4 | 5 | renderHandlers.onload = function() { 6 | var entity = Ophal.settings.entity.current; 7 | var core = Ophal.settings.core; 8 | 9 | $('#content').append('' + Ophal.t('Loading...') + ''); 10 | 11 | /* Fetch current content comments */ 12 | $.ajax({ 13 | type: 'GET', 14 | url: core.base.route + 'comment/fetch/' + entity.id, 15 | contentType: 'application/json; charset=utf-8', 16 | processData: false, 17 | success: function (data) { 18 | if (data.success) { 19 | var count = 0; 20 | var wrapper = 21 | $('
    ' + 22 | Ophal.t('There are no comments.') + '
    ') 23 | ; 24 | for (k in data.list) { 25 | if (count == 0) { 26 | $('.no-comments', wrapper).remove(); 27 | } 28 | $(wrapper).prepend(data.list[k].rendered); 29 | count++; 30 | } 31 | $('#content .loader').remove(); 32 | $('#content').append(wrapper); 33 | Ophal.scroll_down(); 34 | $(document).trigger('ophal:comments:load', [$('.comments-wrapper'), data]); 35 | } 36 | else { 37 | Ophal.set_message('Comments not available.'); 38 | } 39 | }, 40 | error: function() { 41 | Ophal.set_message('Error loading comments.'); 42 | }, 43 | }); 44 | } 45 | 46 | renderHandlers.onclick = function() { 47 | var wrapper = $(''+ Ophal.t('Show comments') + ''); 48 | $('#content > div').append(wrapper); 49 | $('#content > div .button').click(function() { 50 | $('#content .button').remove(); 51 | renderHandlers.onload(); 52 | }); 53 | } 54 | 55 | $(document).ready(function() { 56 | var config = Ophal.settings.comment; 57 | 58 | /* Load comments if current page is an entity */ 59 | if ('entity' in Ophal.settings) { 60 | renderHandlers[config.render_handler](); 61 | } 62 | 63 | $('.comment-form').submit(function() { 64 | var id = $(this).attr('entity:id'); 65 | var entityId = $(this).attr('entity:entity_id'); 66 | var parentId = $(this).attr('entity:parent_id'); 67 | 68 | var endpoint = '/comment/save'; 69 | if (id) { 70 | endpoint += '/' + id; 71 | } 72 | 73 | var entity = { 74 | type: 'comment', 75 | entity_id: entityId, 76 | parent_id: parentId, 77 | body: $('textarea', this).val(), 78 | } 79 | $(document).trigger('ophal:entity:save', {context: this, entity: entity}); 80 | 81 | /* Submit data */ 82 | $.ajax({ 83 | type: 'POST', 84 | url: endpoint, 85 | contentType: 'application/json; charset=utf-8', 86 | data: JSON.stringify(entity), 87 | dataType: 'json', 88 | processData: false, 89 | success: function (data) { 90 | if (data.success) { 91 | window.location = data.return_path + '#comment-' + data.id; 92 | } 93 | else { 94 | $(this).removeAttr('disabled'); 95 | if (data.error) { 96 | alert('Operation failed! Reason: ' + data.error); 97 | } 98 | else { 99 | alert('Operation failed!'); 100 | } 101 | } 102 | }, 103 | error: function() { 104 | alert('Operation error. Please try again later.'); 105 | }, 106 | }); 107 | 108 | return false; 109 | }); 110 | $('.comment-form textarea').keydown(function(event) { 111 | if (event.keyCode == 13) { 112 | $(this).attr('disabled', 'disabled'); 113 | event.preventDefault(); 114 | event.returnValue = false; 115 | $(this).closest("form").submit(); 116 | } 117 | }) 118 | }); 119 | 120 | }); 121 | -------------------------------------------------------------------------------- /modules/comment/comment.tpl.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | 5 |
    6 |
    7 | -------------------------------------------------------------------------------- /modules/comment/init.lua: -------------------------------------------------------------------------------- 1 | local config = settings.comment or {} 2 | if config.render_handler == nil then config.render_handler = 'onload' end 3 | local add_js, theme, header, arg, env, l = add_js, theme, header, route_arg, env, l 4 | local modules, tonumber, empty = ophal.modules, tonumber, seawolf.variable.empty 5 | local request_get_body, json, type = request_get_body, require 'dkjson', type 6 | local _SESSION, time, module_invoke_all = _SESSION, os.time, module_invoke_all 7 | local pairs, render_t, url = pairs, render_t, url 8 | 9 | local debug = debug 10 | 11 | module 'ophal.modules.comment' 12 | 13 | local user_mod, db_query, db_last_insert_id 14 | 15 | --[[ Implements hook init(). 16 | ]] 17 | function init() 18 | db_query = env.db_query 19 | db_last_insert_id = env.db_last_insert_id 20 | user_mod = modules.user 21 | end 22 | 23 | --[[ Implements hook entity_render(). 24 | ]] 25 | function entity_render(entity) 26 | if not config.entities[entity.type] then return end 27 | 28 | add_js 'modules/comment/comment.js' 29 | add_js {type = 'settings', namespace = 'entity', {current = {id = entity.id}}} 30 | add_js {type = 'settings', namespace = 'comment', config} 31 | 32 | local links 33 | 34 | if comment_access(nil, 'create') then 35 | if entity.links == nil then entity.links = {} end 36 | links = entity.links 37 | links[1 + #links] = l( 38 | 'Add a new comment', 39 | 'comment/create/' .. entity.id, 40 | {attributes = {rel = 'nofollow'}} 41 | ) 42 | end 43 | end 44 | 45 | --[[ Implements hook route(). 46 | ]] 47 | function route() 48 | local items = {} 49 | 50 | items['comment/create'] = { 51 | title = 'Add a comment', 52 | page_callback = 'create_form', 53 | } 54 | 55 | items['comment/save'] = { 56 | page_callback = 'save_service', 57 | format = 'json', 58 | } 59 | 60 | items['comment/fetch'] = { 61 | page_callback = 'fetch_service', 62 | format = 'json', 63 | } 64 | 65 | return items 66 | end 67 | 68 | function load(id) 69 | local rs, err, entity 70 | 71 | id = tonumber(id or 0) 72 | 73 | rs, err = db_query('SELECT * FROM comment WHERE id = ?', id) 74 | if err then 75 | error(err) 76 | end 77 | 78 | entity = rs:fetch(true) 79 | 80 | if entity then 81 | entity.type = 'comment' 82 | module_invoke_all('entity_load', entity) 83 | end 84 | 85 | return entity 86 | end 87 | 88 | function load_multiple_by(field_name, value) 89 | local rs, err 90 | local rows = {} 91 | 92 | rs, err = db_query('SELECT * FROM comment WHERE ' .. field_name .. ' = ?', value) 93 | 94 | for row in rs:rows(true) do 95 | rows[1 + #rows] = row 96 | end 97 | 98 | return rows, err 99 | end 100 | 101 | function comment_access(entity, action) 102 | local account = user_mod.current() 103 | 104 | if user_mod.access 'administer comments' then 105 | return true 106 | end 107 | 108 | if action == 'create' then 109 | return user_mod.access 'post comments' 110 | elseif action == 'update' then 111 | return user_mod.access 'edit own comments' and entity.user_id == account.id 112 | elseif action == 'read' then 113 | return user_mod.access 'access comments' 114 | elseif action == 'delete' then 115 | return user_mod.access 'delete own comments' and entity.user_id == account.id 116 | end 117 | end 118 | 119 | function create_form() 120 | local entity_id, parent_id 121 | 122 | add_js 'modules/comment/comment.js' 123 | 124 | entity_id = tonumber(arg(2) or '') 125 | parent_id = tonumber(arg(3) or '') 126 | 127 | if entity_id then 128 | return theme{'form', id = 'comment_create_form', 129 | attributes = { 130 | class = 'comment-form', 131 | ['entity:entity_id'] = entity_id, 132 | ['entity:parent_id'] = parent_id, 133 | }, 134 | elements = { 135 | {'textarea', description = 'Press ENTER to post.'}, 136 | } 137 | } 138 | else 139 | header('status', 401) 140 | return '' 141 | end 142 | end 143 | 144 | function fetch_service() 145 | local output, entity, entity_id, err 146 | 147 | output = {success = false} 148 | 149 | if not comment_access(comment, 'read') then 150 | header('status', 401) 151 | else 152 | entity_id = arg(2) 153 | if entity_id then 154 | list, err = load_multiple_by('entity_id', entity_id) 155 | if err then 156 | output.error = err 157 | else 158 | for k, row in pairs(list) do 159 | list[k].rendered = render_t{'comment', entity = row, 160 | account = user_mod.load(row.user_id), 161 | author = theme{'author', entity = row}, 162 | } 163 | end 164 | output.list = list 165 | output.success = true 166 | end 167 | end 168 | end 169 | 170 | return output 171 | end 172 | 173 | function save_service() 174 | local _, input, parsed, pos, err, output, account, action, id 175 | 176 | id = tonumber(arg(2) or '') 177 | action = empty(id) and 'create' or 'update' 178 | output = {success = false} 179 | 180 | comment = load(id) 181 | 182 | if not comment_access(comment, action) then 183 | header('status', 401) 184 | elseif action == 'update' and empty(comment) then 185 | header('status', 404) 186 | output.error = 'No such comment.' 187 | else 188 | output.success = false 189 | input = request_get_body() 190 | parsed, pos, err = json.decode(input, 1, nil) 191 | if err then 192 | output.error = err 193 | elseif 194 | 'table' == type(parsed) and 195 | not empty(parsed) and 196 | not empty(parsed.entity_id) 197 | then 198 | parsed.id = id 199 | parsed.type = 'comment' 200 | 201 | parsed.status = 1 -- Make comments public by default 202 | 203 | _, err = module_invoke_all('entity_before_save', parsed) 204 | 205 | if err then 206 | output.error = err 207 | else 208 | if action == 'create' then 209 | id, err = create(parsed) 210 | elseif action == 'update' then 211 | _, err = update(parsed) 212 | end 213 | 214 | if err then 215 | output.error = err 216 | else 217 | output.id = id 218 | output.return_path = url('content/' .. parsed.entity_id) 219 | output.success = true 220 | end 221 | end 222 | end 223 | end 224 | 225 | return output 226 | end 227 | 228 | function create(entity) 229 | local rs, err 230 | 231 | if entity.type == nil then entity.type = 'comment' end 232 | 233 | if entity.id then 234 | rs, err = db_query([[ 235 | INSERT INTO comment(id, entity_id, parent_id, user_id, language, body, created, status, sticky) 236 | VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)]], 237 | entity.id, 238 | entity.entity_id, 239 | entity.parent_id, 240 | entity.user_id or user_mod.current().id, 241 | entity.language or 'en', 242 | entity.body, 243 | entity.created or time(), 244 | entity.status, 245 | entity.sticky or false 246 | ) 247 | else 248 | rs, err = db_query([[ 249 | INSERT INTO comment(entity_id, parent_id, user_id, language, body, created, status, sticky) 250 | VALUES(?, ?, ?, ?, ?, ?, ?, ?)]], 251 | entity.entity_id, 252 | entity.parent_id, 253 | entity.user_id or user_mod.current().id, 254 | entity.language or 'en', 255 | entity.body, 256 | entity.created or time(), 257 | entity.status, 258 | entity.sticky or false 259 | ) 260 | entity.id = db_last_insert_id('comment', 'id') 261 | end 262 | 263 | if not err then 264 | module_invoke_all('entity_after_save', entity) 265 | end 266 | return entity.id, err 267 | end 268 | 269 | function update(entity) 270 | local rs, err 271 | rs, err = db_query('UPDATE comment SET body = ?, status = ?, changed = ? WHERE id = ?', entity.body, entity.status, time(), entity.id) 272 | if not err then 273 | module_invoke_all('entity_after_save', entity) 274 | end 275 | return rs, err 276 | end 277 | -------------------------------------------------------------------------------- /modules/content/content.js: -------------------------------------------------------------------------------- 1 | Ophal.extend('content', function($) { 2 | 3 | $(document).ready(function() { 4 | var form = $('#content_edit_form, #content_create_form'); 5 | 6 | $('#save_submit', form).click(function() { 7 | var id = $('#content_edit_form #entity_id').val(); 8 | 9 | var endpoint = '/content/save'; 10 | if (id) { 11 | endpoint += '/' + id; 12 | } 13 | 14 | var content = { 15 | title: $('#content_title', form).val(), 16 | teaser: $('#content_teaser', form).val(), 17 | body: $('#content_body', form).val(), 18 | status: $('#content_status', form).is(':checked'), 19 | promote: $('#content_promote', form).is(':checked'), 20 | } 21 | $(document).trigger('ophal:entity:save', {context: form, entity: content}); 22 | 23 | $.ajax({ 24 | type: 'POST', 25 | url: endpoint, 26 | contentType: 'application/json; charset=utf-8', 27 | data: JSON.stringify(content), 28 | dataType: 'json', 29 | processData: false, 30 | success: function (data) { 31 | if (data.success) { 32 | window.location = '/content/' + data.id; 33 | } 34 | else { 35 | if (data.error) { 36 | alert('Operation failed! Reason: ' + data.error); 37 | } 38 | else { 39 | alert('Operation failed!'); 40 | } 41 | } 42 | }, 43 | error: function() { 44 | alert('Operation error. Please try again later.'); 45 | }, 46 | }); 47 | }); 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /modules/content/content_page.tpl.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 |
    7 | -------------------------------------------------------------------------------- /modules/content/content_teaser.tpl.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /modules/content/init.lua: -------------------------------------------------------------------------------- 1 | local config = settings.content 2 | local env, theme, _GET, tonumber, ceil = env, theme, _GET, tonumber, math.ceil 3 | local tinsert, tconcat, pairs, debug = table.insert, table.concat, pairs, debug 4 | local pager, l, page_set_title, arg = pager, l, page_set_title, route_arg 5 | local tonumber, format_date = tonumber, format_date 6 | local empty, add_js, ophal, t = seawolf.variable.empty, add_js, ophal, t 7 | local header, json, type, time = header, require 'dkjson', type, os.time 8 | local print_t, require, modules = print_t, require, ophal.modules 9 | local module_invoke_all, request_get_body = module_invoke_all, request_get_body 10 | local error = error 11 | 12 | local set_global = set_global 13 | 14 | module 'ophal.modules.content' 15 | 16 | local user_mod, db_query, db_limit, db_last_insert_id 17 | 18 | --[[ Implements hook init(). 19 | ]] 20 | function init() 21 | db_query = env.db_query 22 | db_limit = env.db_limit 23 | db_last_insert_id = env.db_last_insert_id 24 | user_mod = modules.user 25 | end 26 | 27 | --[[ Implements hook route(). 28 | ]] 29 | function route() 30 | items = {} 31 | items.content = { 32 | page_callback = 'router', 33 | } 34 | items['content/save'] = { 35 | page_callback = 'save_service', 36 | format = 'json', 37 | } 38 | return items 39 | end 40 | 41 | function load(id) 42 | local rs, err, entity 43 | 44 | id = tonumber(id or 0) 45 | 46 | rs, err = db_query('SELECT * FROM content WHERE id = ?', id) 47 | if err then 48 | error(err) 49 | end 50 | 51 | entity = rs:fetch(true) 52 | 53 | if entity then 54 | entity.type = 'content' 55 | module_invoke_all('entity_load', entity) 56 | end 57 | 58 | return entity 59 | end 60 | 61 | function entity_access(entity, action) 62 | local account = user_mod.current() 63 | 64 | if user_mod.access 'administer content' then 65 | return true 66 | end 67 | 68 | if action == 'create' then 69 | return user_mod.access 'create content' 70 | elseif action == 'update' then 71 | return user_mod.access 'edit own content' and entity.user_id == account.id 72 | elseif action == 'read' then 73 | return user_mod.access 'access content' 74 | elseif action == 'delete' then 75 | return user_mod.access 'delete own content' and entity.user_id == account.id 76 | end 77 | end 78 | 79 | function save_service() 80 | local input, parsed, pos, err, output, account, action, id 81 | local entity 82 | 83 | if not user_mod.is_logged_in() then 84 | header('status', 401) 85 | else 86 | id = tonumber(arg(2) or '') 87 | action = empty(id) and 'create' or 'update' 88 | output = {} 89 | 90 | entity = load(id) 91 | 92 | if not entity_access(entity, action) then 93 | header('status', 401) 94 | elseif action == 'update' and empty(entity) then 95 | header('status', 404) 96 | output.error = 'No such content.' 97 | else 98 | output.success = false 99 | input = request_get_body() 100 | parsed, pos, err = json.decode(input, 1, nil) 101 | if err then 102 | output.error = err 103 | elseif 'table' == type(parsed) and not empty(parsed) then 104 | parsed.id = id 105 | parsed.type = 'content' 106 | 107 | if type(parsed.status) == 'boolean' then 108 | parsed.status = parsed.status and 1 or 0 109 | end 110 | if type(parsed.promote) == 'boolean' then 111 | parsed.promote = parsed.promote and 1 or 0 112 | end 113 | 114 | if action == 'create' then 115 | id, err = create(parsed) 116 | elseif action == 'update' then 117 | do _, err = update(parsed) end 118 | end 119 | 120 | if err then 121 | output.error = err 122 | else 123 | output.id = id 124 | output.success = true 125 | end 126 | end 127 | end 128 | end 129 | 130 | return output 131 | end 132 | 133 | function create(entity) 134 | local rs, err 135 | 136 | if entity.type == nil then entity.type = 'content' end 137 | 138 | if entity.id then 139 | rs, err = db_query([[ 140 | INSERT INTO content(id, user_id, title, teaser, body, status, promote, created) 141 | VALUES(?, ?, ?, ?, ?, ?, ?, ?)]], 142 | entity.id, 143 | entity.user_id or user_mod.current().id, 144 | entity.title, 145 | entity.teaser, 146 | entity.body, 147 | entity.status, 148 | entity.promote or false, 149 | entity.created or time() 150 | ) 151 | else 152 | rs, err = db_query([[ 153 | INSERT INTO content(user_id, title, teaser, body, status, promote, created) 154 | VALUES(?, ?, ?, ?, ?, ?, ?)]], 155 | entity.user_id or user_mod.current().id, 156 | entity.title, 157 | entity.teaser, 158 | entity.body, 159 | entity.status, 160 | entity.promote or false, 161 | entity.created or time() 162 | ) 163 | entity.id = db_last_insert_id('content', 'id') 164 | end 165 | 166 | if not err then 167 | module_invoke_all('entity_after_save', entity) 168 | end 169 | return entity.id, err 170 | end 171 | 172 | function update(entity) 173 | local rs, err 174 | rs, err = db_query('UPDATE content SET title = ?, teaser = ?, body = ?, status = ?, promote = ?, changed = ? WHERE id = ?', entity.title, entity.teaser, entity.body, entity.status, entity.promote, time(), entity.id) 175 | if not err then 176 | module_invoke_all('entity_after_save', entity) 177 | end 178 | return rs, err 179 | end 180 | 181 | function router() 182 | local rs, err, ipp, current_page, num_pages, count, entity, id, arg1 183 | local account = user_mod.current() 184 | 185 | arg1 = arg(1) 186 | 187 | if not empty(arg1) then 188 | if arg1 == 'create' then 189 | if not entity_access(entity, 'create') then 190 | page_set_title 'Access denied' 191 | header('status', 401) 192 | return '' 193 | end 194 | 195 | add_js 'libraries/jquery.min.js' 196 | add_js 'libraries/json2.js' 197 | add_js 'modules/content/content.js' 198 | 199 | page_set_title 'Create content' 200 | return theme{'content_form'} 201 | end 202 | 203 | entity = load(arg1) 204 | 205 | if empty(entity) then 206 | page_set_title 'Page not found' 207 | header('status', 404) 208 | return '' 209 | elseif not entity_access(entity, 'read') then 210 | page_set_title 'Access denied' 211 | header('status', 401) 212 | return '' 213 | end 214 | 215 | if arg(2) == 'edit' then 216 | if not entity_access(entity, 'update') then 217 | page_set_title 'Access denied' 218 | header('status', 401) 219 | return '' 220 | end 221 | 222 | add_js 'libraries/jquery.min.js' 223 | add_js 'libraries/json2.js' 224 | add_js 'modules/content/content.js' 225 | page_set_title('Edit "' .. entity.title .. '"') 226 | 227 | return theme{'content_form', entity = entity} 228 | else 229 | page_set_title(entity.title) 230 | if not empty(entity.status) or entity.user_id == account.id or user_mod.access 'administer content' then 231 | page_set_title(entity.title) 232 | set_global('language', entity.language) 233 | module_invoke_all('entity_render', entity) 234 | return function () 235 | print_t{'content_page', 236 | account = user_mod.load(entity.user_id) or user_mod.load(0), 237 | entity = entity, 238 | format_date = format_date 239 | } 240 | end 241 | else 242 | page_set_title 'Access denied' 243 | header('status', 401) 244 | return '' 245 | end 246 | end 247 | else 248 | return frontpage() 249 | end 250 | end 251 | 252 | function frontpage() 253 | local rows = {} 254 | local rs, err, count, current_page, ipp, num_pages, query 255 | 256 | -- Count rows 257 | query = ('SELECT count(*) FROM content WHERE promote = 1 %s'):format(user_mod.is_logged_in() and '' or 'AND status = 1') 258 | rs, err = db_query(query) 259 | if err then 260 | error(err) 261 | else 262 | count = (rs:fetch() or {})[1] 263 | end 264 | 265 | -- Calculate current page 266 | current_page = tonumber(_GET.page) or 1 267 | ipp = config.items_per_page or 10 268 | num_pages = ceil(count/ipp) 269 | 270 | -- Render list 271 | query = ('SELECT * FROM content WHERE promote = 1 %s ORDER BY created DESC' .. db_limit()):format(user_mod.is_logged_in() and '' or 'AND status = 1') 272 | rs, err = db_query(query, (current_page -1)*ipp, ipp) 273 | if err then 274 | error(err) 275 | else 276 | for row in rs:rows(true) do 277 | tinsert(rows, function () print_t{'content_teaser', entity = row} end) 278 | end 279 | end 280 | 281 | if num_pages > 1 then 282 | page_set_title(("%s (page %s)"):format(t('Frontpage'), _GET.page or 1)) 283 | end 284 | 285 | return function () 286 | print_t{'content_frontpage', rows = rows} 287 | print_t{'pager', pages = pager('content', num_pages, current_page)} 288 | end 289 | end 290 | 291 | function theme.content_links(variables) 292 | local page, entity, links 293 | 294 | page = variables.page 295 | if page == nil then page = false end 296 | 297 | entity = variables.entity 298 | if entity == nil then entity = {} end 299 | 300 | links = entity.links 301 | if links == nil then links = {} end 302 | 303 | if not page then 304 | links[1 + #links] = l('Read more', 'content/' .. entity.id) 305 | end 306 | 307 | if entity_access(entity, 'update') then 308 | links[1 + #links] = l('edit', 'content/' .. entity.id .. '/edit') 309 | end 310 | 311 | return theme{'item_list', list = links, class = 'content-links'} 312 | end 313 | 314 | function theme.content_frontpage(variables) 315 | local rows = variables.rows 316 | 317 | local output = {} 318 | 319 | for _, row in pairs(rows) do 320 | row() 321 | end 322 | end 323 | 324 | function theme.content_form(variables) 325 | local entity = variables.entity 326 | 327 | if entity == nil then entity = {} end 328 | 329 | return theme{'form', method = 'POST', 330 | attributes = {id = empty(entity.id) and 'content_create_form' or 'content_edit_form'}, 331 | entity = entity, 332 | elements = { 333 | {'hidden', attributes = {id = 'entity_id'}, value = entity.id}, 334 | {'textfield', title = 'Title', attributes = {id = 'content_title', size = 60}, value = entity.title, weight = 10}, 335 | {'textarea', title = 'Teaser', attributes = {id = 'content_teaser', cols = 60, rows = 10}, value = entity.teaser, weight = 20}, 336 | {'textarea', title = 'Body', attributes = {id = 'content_body', cols = 60, rows = 15}, value = entity.body, weight = 30}, 337 | {'checkbox', title = 'Status', attributes = {id = 'content_status'}, value = entity.status, weight = 40}, 338 | {'checkbox', title = 'Promote to frontpage', attributes = {id = 'content_promote'}, value = entity.promote, weight = 50}, 339 | {'markup', title = 'Created on', value = entity.created and format_date(entity.created) or '', weight = 60}, 340 | {'button', attributes = {id = 'save_submit'}, value = 'Save', weight = 70}, 341 | }, 342 | } 343 | end 344 | -------------------------------------------------------------------------------- /modules/file/file.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Derived from: https://github.com/mailopl/html5-xhr2-chunked-file-upload-slice 3 | * by Marcin Wawrzyniak 4 | */ 5 | 6 | (function ($) { 7 | $(document).ready(function () { 8 | $('.form-upload-button').click(function() { 9 | var context = $(this).parent(); 10 | var element = $('.form-upload-file', context).get(0); 11 | var statusDiv = $('.form-upload-status', context); 12 | 13 | /* Clear status messages */ 14 | $(statusDiv).html(''); 15 | 16 | if (element.files[0] != undefined) { 17 | sendRequest(element, context); 18 | $('button', context).attr('disabled', 'disabled'); 19 | } 20 | else { 21 | alert('Please select a file to upload.'); 22 | } 23 | }); 24 | 25 | $('.form-delete-button').click(function() { 26 | var context = $(this).parent(); 27 | var element = $('.form-upload-file', context); 28 | var statusDiv = $('.form-upload-status', context); 29 | 30 | /* Clear status messages */ 31 | $(statusDiv).html(''); 32 | 33 | element.entity_id = $('.form-upload-entity-id', context).val(); 34 | if (element.entity_id) { 35 | deleteFile(element, context); 36 | $('button', context).attr('disabled', 'disabled'); 37 | } 38 | else { 39 | $(statusDiv).html('There is no file to delete!'); 40 | } 41 | }); 42 | }); 43 | 44 | const BYTES_PER_CHUNK = Ophal.settings.core.BYTES_PER_CHUNK; 45 | 46 | /** 47 | * Calculates slices and indirectly uploads a chunk of a file via uploadFile() 48 | */ 49 | function sendRequest(element, context) { 50 | var blob = element.files[0]; 51 | blob.start = 0; 52 | blob.index = 0; 53 | blob.slices = 0; /* slices, value that gets decremented */ 54 | blob.slicesTotal = 0; /* total amount of slices, constant once calculated */ 55 | blob.uniq_id = uuid(); /* file unique identifier, used server side */ 56 | 57 | /* calculate the number of slices */ 58 | blob.slices = Math.ceil(blob.size / BYTES_PER_CHUNK); 59 | blob.slicesTotal = blob.slices; 60 | 61 | uploadContinue(blob, context); 62 | } 63 | 64 | function uploadContinue(blob, context) { 65 | if (blob.start < blob.size) { 66 | blob.end = blob.start + BYTES_PER_CHUNK; 67 | if (blob.end > blob.size) { 68 | blob.end = blob.size; 69 | } 70 | 71 | uploadFile(blob, context); 72 | 73 | blob.start = blob.end; 74 | blob.index++; 75 | } 76 | } 77 | 78 | /** 79 | * Blob to ArrayBuffer (needed ex. on Android 4.0.4) 80 | */ 81 | var str2ab_blobreader = function(str, callback) { 82 | var blob; 83 | BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder; 84 | if (typeof(BlobBuilder) !== 'undefined') { 85 | var bb = new BlobBuilder(); 86 | bb.append(str); 87 | blob = bb.getBlob(); 88 | } 89 | else { 90 | blob = new Blob([str]); 91 | } 92 | var f = new FileReader(); 93 | f.onload = function(e) { 94 | callback(e.target.result) 95 | } 96 | f.readAsArrayBuffer(blob); 97 | } 98 | 99 | /** 100 | * Performs actual upload, adjusts progress bars 101 | * 102 | * @param blob 103 | * @param context 104 | */ 105 | function uploadFile(blob, context) { 106 | var chunk; 107 | var fileData; 108 | var endpoint = "/file/upload?" + 109 | "name=" + encodeURIComponent(blob.name) + "&" + /* filename */ 110 | "id=" + blob.uniq_id + "&" + 111 | "index=" + blob.index /* part identifier */ 112 | ; 113 | 114 | if (blob.webkitSlice) { 115 | chunk = blob.webkitSlice(blob.start, blob.end); 116 | } 117 | else if (blob.mozSlice) { 118 | chunk = blob.mozSlice(blob.start, blob.end); 119 | } 120 | else { 121 | chunk = blob.slice(blob.start, blob.end); 122 | } 123 | 124 | if (blob.webkitSlice) { /* android default browser in version 4.0.4 has webkitSlice instead of slice() */ 125 | var buffer = str2ab_blobreader(chunk, function(buf) { /* we cannot send a blob, because body payload will be empty */ 126 | fileData = buf; /* thats why we send an ArrayBuffer */ 127 | }); 128 | } 129 | else { 130 | fileData = chunk; /* but if we support slice() everything should be ok */ 131 | } 132 | 133 | var statusDiv = $('.form-upload-status', context); 134 | var progressBar = $('.form-upload-progress', context); 135 | 136 | $.ajax({ 137 | url: endpoint, 138 | type: 'POST', 139 | /* Ajax events */ 140 | success: function(data) { 141 | if (data.success) { 142 | blob.slices--; 143 | 144 | if (blob.slices == 0) { 145 | /* merge slices finished all slices */ 146 | mergeFile(blob, context); 147 | } 148 | else { 149 | /* otherwise keep uploading */ 150 | uploadContinue(blob, context); 151 | } 152 | 153 | if (blob.slices > 1) { 154 | /* progress bar */ 155 | $(progressBar).attr('max', blob.slicesTotal); 156 | $(progressBar).val(blob.index); 157 | $(statusDiv).html(Math.round(blob.index/blob.slicesTotal * 100) + "%"); 158 | } 159 | } 160 | else { 161 | /* Allow to try again */ 162 | $('button', context).removeAttr('disabled'); 163 | 164 | if (data.error) { 165 | $(statusDiv).html('Operation failed! Reason: ' + data.error + ''); 166 | } 167 | else { 168 | $(statusDiv).html('Operation failed!'); 169 | } 170 | } 171 | }, 172 | error: function() { 173 | $(statusDiv).html('Operation error. Please try again later.'); 174 | }, 175 | /* File data */ 176 | data: fileData, 177 | /* Options to tell JQuery not to process data or worry about content-type */ 178 | cache: false, 179 | contentType: false, 180 | processData: false 181 | }); 182 | } 183 | 184 | /** 185 | * Function executed once all of the slices has been sent, "TO MERGE THEM ALL!" 186 | */ 187 | function mergeFile(blob, context) { 188 | var endpoint = "/file/merge?" + 189 | "name=" + encodeURIComponent(blob.name) + "&" + /* filename */ 190 | "id=" + blob.uniq_id + "&" + /* unique upload identifier */ 191 | "size=" + blob.size + "&" + /* full size */ 192 | "index=" + blob.slicesTotal /* part identifier */ 193 | ; 194 | 195 | var statusDiv = $('.form-upload-status', context); 196 | var progressBar = $('.form-upload-progress', context); 197 | 198 | /* Fetch auth token */ 199 | $.ajax({ 200 | type: 'GET', 201 | url: endpoint, 202 | success: function (data) { 203 | if (data.success) { 204 | $('.form-upload-entity-id', context).val(data.id); 205 | 206 | $(progressBar).attr('max', 100); 207 | $(progressBar).val(100); 208 | $(statusDiv).html('File uploaded successfully!'); 209 | } 210 | else { 211 | /* Allow to try again */ 212 | $('button', context).removeAttr('disabled'); 213 | 214 | if (data.error) { 215 | $(statusDiv).html('Operation failed! Reason: ' + data.error + ''); 216 | } 217 | else { 218 | $(statusDiv).html('Operation failed!'); 219 | } 220 | } 221 | }, 222 | error: function() { 223 | $(statusDiv).html('Operation error. Please try again later.'); 224 | }, 225 | }); 226 | } 227 | 228 | /** 229 | */ 230 | function deleteFile(element, context) { 231 | var endpoint = "/file/delete?" + 232 | "id=" + element.entity_id 233 | ; 234 | 235 | var statusDiv = $('.form-upload-status', context); 236 | 237 | /* Fetch auth token */ 238 | $.ajax({ 239 | type: 'GET', 240 | url: endpoint, 241 | success: function (data) { 242 | if (data.success) { 243 | $('.form-upload-entity-id', context).val('deleted'); 244 | $(statusDiv).html('File deleted successfully!'); 245 | $('.form-delete-button', context).hide(); 246 | } 247 | else { 248 | /* Allow to try again */ 249 | $('button', context).removeAttr('disabled'); 250 | 251 | if (data.error) { 252 | $(statusDiv).html('Operation failed! Reason: ' + data.error + ''); 253 | } 254 | else { 255 | $(statusDiv).html('Operation failed!'); 256 | } 257 | } 258 | }, 259 | error: function() { 260 | $(statusDiv).html('Operation error. Please try again later.'); 261 | }, 262 | }); 263 | } 264 | })(jQuery); 265 | -------------------------------------------------------------------------------- /modules/file/init.lua: -------------------------------------------------------------------------------- 1 | local seawolf = require 'seawolf'.__build('fs', 'behaviour', 'variable') 2 | local config, theme, header, _GET = settings.file or {}, theme, header, _GET 3 | local tinsert, tconcat, lfs, env = table.insert, table.concat, lfs, env 4 | local is_dir, is_file, add_js = seawolf.fs.is_dir, seawolf.fs.is_file, add_js 5 | local temp_dir, empty = seawolf.behaviour.temp_dir, seawolf.variable.empty 6 | local request_get_body, io_open, tonumber = request_get_body, io.open, tonumber 7 | local json, files_path = require 'dkjson', settings.site.files_path 8 | local os_remove, modules, time = os.remove, ophal.modules, os.time 9 | local module_invoke_all, finfo = module_invoke_all, seawolf.fs.finfo 10 | local render_attributes, format_size = render_attributes, format_size 11 | local format_date, sleep = format_date, socket.sleep 12 | 13 | local debug = debug 14 | 15 | module 'ophal.modules.file' 16 | 17 | local user_mod, db_query, db_last_insert_id 18 | 19 | --[[ Implements hook init(). 20 | ]] 21 | function init() 22 | db_query = env.db_query 23 | db_last_insert_id = env.db_last_insert_id 24 | user_mod = modules.user 25 | end 26 | 27 | --[[ Implements hook route(). 28 | ]] 29 | function route() 30 | items = {} 31 | items['file/upload'] = { 32 | page_callback = 'upload_service', 33 | access_callback = {module = 'user', 'access', 'upload files'}, 34 | format = 'json', 35 | } 36 | items['file/merge'] = { 37 | page_callback = 'merge_service', 38 | access_callback = {module = 'user', 'access', 'upload files'}, 39 | format = 'json', 40 | } 41 | items['file/delete'] = { 42 | page_callback = 'delete_service', 43 | access_callback = {module = 'user', 'access', 'delete own files'}, 44 | format = 'json', 45 | } 46 | return items 47 | end 48 | 49 | function load_by_field(field, value) 50 | if field == nil then field = 'id' end 51 | 52 | local rs, err 53 | 54 | if field == 'id' then 55 | value = tonumber(value or 0) 56 | end 57 | 58 | rs, err = db_query('SELECT * FROM file WHERE ' .. field .. ' = ?', value) 59 | if err then 60 | error(err) 61 | end 62 | 63 | entity = rs:fetch(true) 64 | 65 | if entity then 66 | entity.type = 'file' 67 | module_invoke_all('entity_load', entity) 68 | end 69 | 70 | return entity or {} 71 | end 72 | 73 | function load(id) 74 | return load_by_field('id', id) 75 | end 76 | 77 | --[[ Implements endpoint callback: upload. 78 | ]] 79 | function upload_service() 80 | local output, target, upload_id, index, upload_dir, err 81 | local status, output_fh, data, file 82 | 83 | -- Reduce errors of type: "tempt to yield across metamethod/C-call boundary" 84 | sleep(0.05) 85 | 86 | upload_id = _GET.id 87 | index = _GET.index 88 | file = { 89 | filename = _GET.name, 90 | } 91 | 92 | output = { 93 | success = false, 94 | } 95 | 96 | if config.filedb_storage then 97 | if not empty(load_by_field('filename', file.filename)) then 98 | output.error = 'File uploaded already!' 99 | return output 100 | end 101 | end 102 | 103 | -- Make sure to have a general uploads directory 104 | upload_dir = ('%s/ophal_uploads'):format(temp_dir()) 105 | if not is_dir(upload_dir) and not is_file(upload_dir) then 106 | status, err = lfs.mkdir(upload_dir) 107 | if err then 108 | output.error = err 109 | end 110 | end 111 | 112 | -- Make sure to have a dedicated folder for uploaded file parts 113 | if empty(err) then 114 | upload_dir = ('%s/%s'):format(upload_dir, upload_id) 115 | if not is_dir(upload_dir) and not is_file(upload_dir) then 116 | status, err = lfs.mkdir(upload_dir) 117 | if err then 118 | output.error = err 119 | end 120 | end 121 | end 122 | 123 | -- Write content 124 | if empty(err) then 125 | target = ('%s/%s.part'):format(upload_dir, index) 126 | data = request_get_body() 127 | output_fh, err = io_open(target, 'w+') 128 | if err then 129 | output.error = err 130 | else 131 | output_fh:write(data) 132 | output_fh:close() 133 | output.success = true 134 | end 135 | end 136 | 137 | return output 138 | end 139 | 140 | --[[ Implements endpoint callback: merge. 141 | ]] 142 | function merge_service() 143 | local output, source_fh, target_fh, index, upload_id, data, err, status 144 | local source_path, file 145 | 146 | upload_id = _GET.id 147 | index = tonumber(_GET.index) 148 | file = { 149 | filename = _GET.name, 150 | filepath = ('%s/%s'):format(files_path, _GET.name), 151 | filesize = tonumber(_GET.size or 0), 152 | } 153 | 154 | output = { 155 | success = false, 156 | } 157 | 158 | target_fh, err = io_open(file.filepath, 'w+') 159 | if err then 160 | output.error = err 161 | elseif index > 0 then 162 | for i = 1, index do 163 | source_path = ('%s/ophal_uploads/%s/%s.part'):format(temp_dir(), upload_id, i - 1) 164 | source_fh = io_open(source_path, 'r') 165 | data, err = source_fh:read '*a' 166 | if err then 167 | output.error = err 168 | else 169 | status, err = target_fh:write(data) 170 | if err then 171 | output.error = err 172 | target_fh:close() 173 | end 174 | source_fh:close() 175 | os_remove(source_path) 176 | end 177 | end 178 | os_remove(('%s/ophal_uploads/%s'):format(temp_dir(), upload_id)) 179 | target_fh:close() 180 | 181 | -- Register the file into the database 182 | if config.filedb_storage then 183 | local mime = finfo.open(finfo.MIME_TYPE, finfo.NO_CHECK_COMPRESS) 184 | local rc = mime:load() 185 | if rc ~= 0 then 186 | output.error = mime:error() 187 | else 188 | file.filemime = mime:file(file.filepath) 189 | file.status = true 190 | file.timestamp = time() 191 | data, err = create(file) 192 | if empty(err) then 193 | output.id = data 194 | else 195 | output.error = err 196 | end 197 | end 198 | end 199 | 200 | output.success = true 201 | end 202 | 203 | return output 204 | end 205 | 206 | function delete_service() 207 | local rs, err 208 | local file_id = _GET.id 209 | local output = {success = false} 210 | 211 | if not empty(file_id) then 212 | entity = load(file_id) 213 | rs, err = delete(entity) 214 | if empty(err) then 215 | output.success = true 216 | else 217 | output.error = err 218 | end 219 | end 220 | 221 | return output 222 | end 223 | 224 | function create(entity) 225 | local rs, err 226 | 227 | if entity.type == nil then entity.type = 'file' end 228 | 229 | rs, err = (function(id, ...) 230 | if id then 231 | return db_query([[ 232 | INSERT INTO file(id, user_id, filename, filepath, filemime, filesize, status, timestamp) 233 | VALUES(?, ?, ?, ?, ?, ?, ?, ?)]], id, ...) 234 | else 235 | local rs1, rs2 = db_query([[ 236 | INSERT INTO file(user_id, filename, filepath, filemime, filesize, status, timestamp) 237 | VALUES(?, ?, ?, ?, ?, ?, ?)]], ...) 238 | entity.id = db_last_insert_id('file', 'id') 239 | return rs1, rs2 240 | end 241 | end)( 242 | entity.id, 243 | entity.user_id or user_mod.current().id, 244 | entity.filename, 245 | entity.filepath, 246 | entity.filemime, 247 | entity.filesize, 248 | entity.status, 249 | entity.timestamp 250 | ) 251 | 252 | if not err then 253 | module_invoke_all('entity_after_save', entity) 254 | end 255 | 256 | return entity.id, err 257 | end 258 | 259 | function update(entity) 260 | local rs, err 261 | rs, err = db_query('UPDATE file SET user_id = ?, filename = ?, filepath = ?, filemime = ?, filesize = ?, status = ?, timestamp = ? WHERE id = ?', 262 | entity.user_id, 263 | entity.filename, 264 | entity.filepath, 265 | entity.filemime, 266 | entity.filesize, 267 | entity.status, 268 | entity.timestamp, 269 | entity.id 270 | ) 271 | if not err then 272 | module_invoke_all('entity_after_save', entity) 273 | end 274 | return rs, err 275 | end 276 | 277 | function delete(entity) 278 | local rs, err 279 | 280 | rs, err = db_query('DELETE FROM file WHERE id = ?', entity.id) 281 | 282 | if not err then 283 | if entity.filepath then 284 | os_remove(entity.filepath) 285 | end 286 | module_invoke_all('entity_after_delete', entity) 287 | end 288 | 289 | return rs, err 290 | end 291 | 292 | function handle_upload(src, tgt) 293 | local src_id = src.entity[src.field] 294 | local tgt_id = tgt.entity[tgt.field] 295 | 296 | if empty(tgt_id) then 297 | -- Keep current value 298 | tgt.entity[tgt.field] = src_id 299 | elseif tgt_id == 'deleted' then 300 | tgt.entity[tgt.field] = nil 301 | end 302 | end 303 | 304 | function theme.file(variables) 305 | if variables == nil then variables = {} end 306 | if variables.attributes == nil then variables.attributes = {} end 307 | 308 | local id, attributes, entity 309 | local file_info, delete_button = '', '' 310 | 311 | add_js 'libraries/uuid.js' 312 | add_js {type = 'settings', { 313 | BYTES_PER_CHUNK = config.bytes_per_chunk or (1024 * 1024),-- 1MB chunk sizes 314 | }} 315 | add_js 'modules/file/file.js' 316 | 317 | id = variables.id 318 | if empty(id) then 319 | id = 'upload' 320 | end 321 | 322 | entity = variables.entity or {} 323 | if not empty(entity) then 324 | file_info = tconcat{ 325 | 'Current file: ', entity.filename, '
    ', 326 | 'Uploaded on: ', format_date(entity.timestamp), 327 | } 328 | end 329 | 330 | return tconcat{ 331 | ('
    '):format(id), 332 | theme{'hidden', attributes = {class = 'form-upload-entity-id'}, value = entity.id}, 333 | '

    ', file_info, '

    ', 334 | (''):format(id, id, render_attributes(variables.attributes)), 335 | theme{'button', value = 'upload', attributes = {class = 'form-upload-button'}}, 336 | theme{'button', value = 'delete', attributes = {class = 'form-delete-button'}}, 337 | '
    ', 338 | '', 339 | '
    Ready to upload
    ', 340 | '
    ' 341 | } 342 | end 343 | 344 | function theme.file_info(variables) 345 | local entity = variables.file 346 | 347 | return tconcat{ 348 | '
    ', 349 | '', entity.filename or '', '', 350 | ' - ', 351 | '', format_size(entity.filesize), '', 352 | ' - ', 353 | '', format_date(entity.timestamp), '', 354 | '
    ', 355 | } 356 | end 357 | -------------------------------------------------------------------------------- /modules/lorem_ipsum/init.lua: -------------------------------------------------------------------------------- 1 | local route_register_alias, url, theme = route_register_alias, url, theme 2 | local page_set_title, l = page_set_title, l 3 | local _SESSION, format, tonumber = _SESSION, string.format, tonumber 4 | 5 | module 'ophal.modules.lorem_ipsum' 6 | 7 | --[[ Implementation of hook init(). 8 | ]] 9 | function init() 10 | route_register_alias('lorem_ipsum', 'loremipsum') 11 | end 12 | 13 | --[[ Implementation of hook route(). 14 | ]] 15 | function route() 16 | local items = {} 17 | items.lorem_ipsum = { 18 | title = 'Lorem Ipsum', 19 | page_callback = 'page', 20 | } 21 | return items 22 | end 23 | 24 | function page() 25 | local title = l('Lorem Ipsum', 'lorem_ipsum') 26 | 27 | page_set_title('Lorem Ipsum', title) 28 | 29 | return theme{'lorem_ipsum'} 30 | end 31 | 32 | function theme.lorem_ipsum() 33 | local counter = '' 34 | if _SESSION then 35 | _SESSION.lorem_ipsum_counter = (_SESSION.lorem_ipsum_counter or 0) + 1 36 | counter = format('

    This page has been shown %s times.

    ', _SESSION.lorem_ipsum_counter) 37 | end 38 | return [[

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tristique arcu sit amet nulla semper cursus. Pellentesque ut augue dui. Suspendisse ac turpis id ante gravida aliquet sed sed nisi. Fusce nec bibendum purus. Vivamus blandit ultrices magna, nec ultrices nulla ullamcorper ac. Donec eu tellus sit amet orci pharetra pharetra vitae vitae tortor. Etiam et ante vel urna mollis iaculis nec eu quam. Proin purus lectus, malesuada id hendrerit ut, pellentesque a justo. Vivamus eget magna risus, sit amet vulputate tellus. In dictum dapibus lorem, in mattis enim dapibus in. Praesent iaculis, nisl nec consequat viverra, libero purus lacinia mi, in condimentum est nisi eu lorem. Maecenas adipiscing aliquam sem, id convallis nunc imperdiet eget. Vestibulum quis sapien sodales eros ultricies adipiscing. Aliquam erat volutpat. Proin venenatis purus eget metus mattis feugiat.

    39 | 40 |

    Ut luctus, orci vitae rhoncus semper, odio ante commodo erat, non pellentesque ipsum odio id arcu. Morbi congue libero nec nunc lobortis sit amet pretium turpis semper. Aliquam erat volutpat. Vestibulum scelerisque varius pulvinar. Etiam molestie diam et lorem sodales tristique. Ut sit amet erat urna. Morbi aliquam vulputate metus vitae dapibus. Ut viverra consectetur nisl eu fermentum. Nunc malesuada rhoncus ante, sed lacinia enim euismod vel. Integer accumsan velit dui, sit amet mattis nulla. In ac felis at magna suscipit auctor ut quis neque. Aliquam cursus gravida egestas.

    41 | 42 |

    Morbi placerat viverra dui, vitae malesuada enim varius at. Donec hendrerit nisl sed ligula iaculis ornare. Donec fringilla vestibulum tristique. Pellentesque eget dui lorem, bibendum consectetur enim. Quisque consequat libero quis enim laoreet condimentum. Etiam sem felis, accumsan at accumsan eget, ultrices ac nulla. Etiam in mollis dolor. Ut a leo nibh. Praesent quis odio et dolor pulvinar adipiscing. Praesent ut sapien sit amet sem pulvinar interdum. Nullam accumsan imperdiet nisi. Pellentesque sodales magna vel tortor eleifend ac molestie dolor mollis. Praesent viverra mollis urna, eget pretium nulla congue nec. Curabitur neque elit, porttitor sit amet pharetra ac, vehicula rutrum lacus.

    ]] .. 43 | counter 44 | end 45 | -------------------------------------------------------------------------------- /modules/menu/init.lua: -------------------------------------------------------------------------------- 1 | local module_invoke_all, empty, l = module_invoke_all, seawolf.variable.empty, l 2 | local tconcat, tinsert, theme, pairs = table.concat, table.insert, theme, pairs 3 | local type, tsort, render_attributes = type, table.sort, render_attributes 4 | 5 | local debug = debug 6 | 7 | module 'ophal.modules.menu' 8 | 9 | local menus = { 10 | primary_links = { 11 | } 12 | } 13 | 14 | local menus_build 15 | 16 | function get_menus(reset) 17 | if reset then 18 | menus_build = false 19 | end 20 | 21 | if not menus_build then 22 | module_invoke_all('menus_alter', menus) 23 | menus_build = true 24 | end 25 | 26 | return menus 27 | end 28 | 29 | function theme.menu(variables) 30 | local menu_id = variables.id 31 | local menu = get_menus()[menu_id] or {} 32 | 33 | local items = {} 34 | local output = {} 35 | 36 | local default_attributes = { 37 | id = 'menu_' .. menu_id, 38 | } 39 | local attributes = render_attributes(variables.attributes, default_attributes) 40 | 41 | if type(menu) == 'function' then 42 | menu = menu() 43 | end 44 | 45 | for route, v in pairs(menu) do 46 | if type(v) ~= 'table' then 47 | v = {v} 48 | end 49 | if v.weight == nil then 50 | v.weight = 0 51 | end 52 | 53 | local label, options 54 | label = v[1] 55 | v[1] = nil 56 | options = v 57 | 58 | tinsert(items, {l(label, route, options), weight = v.weight}) 59 | end 60 | 61 | tsort(items, function (a, b) 62 | return a.weight < b.weight 63 | end) 64 | 65 | output = { 66 | '', 67 | (function (items) 68 | local output = {} 69 | for k, v in pairs(items) do 70 | tinsert(output, v[1]) 71 | end 72 | return tconcat(output, ' | ') 73 | end)(items), 74 | '', 75 | } 76 | 77 | return tconcat(output) 78 | end 79 | -------------------------------------------------------------------------------- /modules/system/init.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | ophal.modules.system = _M 3 | 4 | function _M.cron() 5 | session_destroy_expired() 6 | end 7 | 8 | return _M 9 | -------------------------------------------------------------------------------- /modules/tag/tag_form.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | $(document).ready(function() { 4 | var baseRoute = Ophal.settings.core.base.route; 5 | 6 | (function(context) { 7 | $('#save_submit', context).click(function() { 8 | var this_button = $(this); 9 | var id = $('#entity_id', context).val(); 10 | var entity = { 11 | name: $('#name_field', context).val(), 12 | description: $('#description_field', context).val(), 13 | status: $('#status_field', context).is(':checked'), 14 | action: $('#action', context).val() 15 | } 16 | var endpoint = 'tag/service'; 17 | 18 | if (id) { 19 | endpoint += '/' + id; 20 | } 21 | 22 | $(this_button).attr('disabled', 'disabled'); 23 | 24 | $.ajax({ 25 | type: 'POST', 26 | url: baseRoute + endpoint, 27 | contentType: 'application/json; charset=utf-8', 28 | data: JSON.stringify(entity), 29 | dataType: 'json', 30 | processData: false, 31 | success: function (data) { 32 | if (data.success) { 33 | window.location = baseRoute + 'tag/' + data.tag_id; 34 | } 35 | else { 36 | $(this_button).removeAttr('disabled'); 37 | if (data.error) { 38 | alert('Operation failed! Reason: ' + data.error); 39 | } 40 | else { 41 | alert('Operation failed!'); 42 | } 43 | } 44 | }, 45 | error: function() { 46 | $(this_button).removeAttr('disabled'); 47 | alert('Operation error. Please try again later.'); 48 | }, 49 | }); 50 | }); 51 | })($('#tag_create_form, #tag_edit_form')); 52 | 53 | (function(context) { 54 | $('#confirm_submit', context).click(function() { 55 | var this_button = $(this); 56 | var file = { 57 | action: 'delete' 58 | } 59 | var endpoint = 'tag/service/' + $('#entity_id', context).val(); 60 | 61 | $(this_button).attr('disabled', 'disabled'); 62 | 63 | $.ajax({ 64 | type: 'POST', 65 | url: baseRoute + endpoint, 66 | contentType: 'application/json; charset=utf-8', 67 | data: JSON.stringify(file), 68 | dataType: 'json', 69 | processData: false, 70 | success: function (data) { 71 | if (data.success) { 72 | window.location = baseRoute + 'admin/content/tags'; 73 | } 74 | else { 75 | $(this_button).removeAttr('disabled'); 76 | if (data.error) { 77 | alert('Operation failed! Reason: ' + data.error); 78 | } 79 | else { 80 | alert('Operation failed!'); 81 | } 82 | } 83 | }, 84 | error: function() { 85 | $(this_button).removeAttr('disabled'); 86 | alert('Operation error. Please try again later.'); 87 | }, 88 | }); 89 | }); 90 | })($('#tag_delete_form')); 91 | 92 | $(document).bind('ophal:entity:save', function(caller, variables) { 93 | var context = variables.context 94 | var entity = variables.entity 95 | 96 | entity.tags = $('#field_tags', context).val(); 97 | }); 98 | }); 99 | 100 | })(jQuery); -------------------------------------------------------------------------------- /modules/test/init.lua: -------------------------------------------------------------------------------- 1 | local theme, add_js = theme, add_js 2 | 3 | module 'ophal.modules.test' 4 | 5 | --[[ Implements hook route(). 6 | ]] 7 | function route() 8 | local items = {} 9 | 10 | items['upload-test'] = { 11 | title = 'Upload a file', 12 | page_callback = 'upload_test_page', 13 | access_callback = {module = 'user', 'is_logged_in'}, 14 | } 15 | 16 | return items 17 | end 18 | 19 | function upload_test_page() 20 | add_js 'libraries/uuid.js' 21 | add_js 'modules/file/file.js' 22 | return 23 | '
    ' .. 24 | theme{'file'} .. 25 | '
    ' 26 | end 27 | -------------------------------------------------------------------------------- /modules/user/admin.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | module 'ophal.modules.user.admin' 4 | 5 | --[[ Implements hook route(). 6 | ]] 7 | function route() 8 | items = {} 9 | items['users'] = { 10 | title = 'User login', 11 | page_callback = 'users_page', 12 | } 13 | return items 14 | end 15 | 16 | function users() 17 | return theme.item_list{list = {}} 18 | end 19 | -------------------------------------------------------------------------------- /modules/user/init.lua: -------------------------------------------------------------------------------- 1 | local seawolf = require 'seawolf'.__build('other', 'variable', 'contrib') 2 | local json, hash, tonumber = require 'dkjson', seawolf.other.hash, tonumber 3 | local print, exit, _SESSION, config = print, exit, env._SESSION, settings.user or {} 4 | local error, empty, header, l = error, seawolf.variable.empty, header, l 5 | local theme, tconcat, add_js, unpack = theme, table.concat, add_js, unpack 6 | local type, env, uuid, time, goto, pairs = type, env, uuid, os.time, goto, pairs 7 | local session_destroy, module_invoke_all = session_destroy, module_invoke_all 8 | local request_get_body, ophal, pcall = request_get_body, ophal, pcall 9 | local route_execute_callback, _GET = route_execute_callback, _GET 10 | local url_parse, _SERVER = socket.url.parse, _SERVER 11 | local xtable = seawolf.contrib.seawolf_table 12 | 13 | module 'ophal.modules.user' 14 | 15 | --[[ Implements hook route(). 16 | ]] 17 | function route() 18 | items = {} 19 | items.user = { 20 | page_callback = 'default_page' 21 | } 22 | items['user/login'] = { 23 | title = 'User login', 24 | page_callback = 'login_page', 25 | access_callback = 'is_anonymous', 26 | } 27 | items['user/logout'] = { 28 | title = 'User logout', 29 | page_callback = 'logout_page', 30 | access_callback = 'is_logged_in', 31 | } 32 | items['user/auth'] = { 33 | title = 'User authentication web service', 34 | page_callback = 'auth_service', 35 | format = 'json', 36 | } 37 | return items 38 | end 39 | 40 | --[[ Implements hook route_validate_handler(). 41 | 42 | NOTE: the access_callback should return false (not just nil) in order to raise 43 | a '401 Access Denied'. 44 | ]] 45 | function route_validate_handler(handler) 46 | local status, result 47 | 48 | status, result = route_execute_callback(handler, 'access_callback') 49 | 50 | if not status then 51 | handler.error = 500 52 | handler.title = 'Unexpected error' 53 | handler.content = ("module '%s': %s"):format(handler.module, result or '') 54 | elseif result == false then 55 | handler.error = 401 56 | handler.title = 'Access denied' 57 | handler.content = handler.title 58 | end 59 | end 60 | 61 | --[[ Implements hook init(). 62 | ]] 63 | function init() 64 | db_query = env.db_query 65 | db_last_insert_id = env.db_last_insert_id 66 | 67 | -- Set anonymous user ID 68 | if nil == _SESSION.user_id then 69 | _SESSION.user_id = 0 70 | end 71 | end 72 | 73 | function is_logged_in() 74 | return not empty(_SESSION.user_id) 75 | end 76 | 77 | function is_anonymous() 78 | return not is_logged_in() 79 | end 80 | 81 | do 82 | local users = {} 83 | 84 | --[[ Build user object for given user ID. 85 | ]] 86 | function load(user_id, reset) 87 | if user_id and empty(users[user_id]) or reset then 88 | users[user_id] = load_by_field('id', user_id) 89 | end 90 | 91 | return users[user_id] 92 | end 93 | end 94 | 95 | function load_by_field(field, value) 96 | local rs, entity 97 | 98 | if field == 'id' and value == 0 then 99 | entity = { 100 | id = 0, 101 | name = 'Anonymous', 102 | } 103 | elseif not empty(field) and not empty(value) then 104 | rs = db_query('SELECT * FROM users WHERE ' .. field .. ' = ?', value) 105 | entity = rs:fetch(true) 106 | end 107 | 108 | if not empty(entity) then 109 | entity.type = 'user' 110 | module_invoke_all('entity_load', entity) 111 | end 112 | 113 | return entity 114 | end 115 | 116 | --[[ Return the list of configured roles. 117 | ]] 118 | do 119 | local roles 120 | 121 | function get_roles(reset) 122 | if nil == config.roles then config.roles = {} end 123 | 124 | if nil == roles or reset then 125 | -- Default roles 126 | roles = { 127 | anonymous = 'Anonymous', 128 | authenticated = 'Authenticated', 129 | } 130 | 131 | -- Load roles from settings 132 | for id, name in pairs(config.roles) do 133 | roles[id] = name 134 | end 135 | 136 | -- Load roles from database storage 137 | if config.permissions_storage then 138 | local rs, err = db_query [[SELECT id, name FROM role WHERE active = 1 ORDER BY weight, id]] 139 | for role in rs:rows(true) do 140 | roles[role.id] = role.name 141 | end 142 | end 143 | end 144 | 145 | return roles 146 | end 147 | end 148 | 149 | do 150 | local users_roles = { 151 | [0] = {anonymous = 'anonymous'}, 152 | } 153 | 154 | --[[ Return the list of roles assigned for given user ID. 155 | ]] 156 | function get_user_roles(user_id, reset) 157 | if nil == users_roles[user_id] or reset then 158 | if empty(config.user_role) then config.user_role = {} end 159 | 160 | local user_roles = {} 161 | local roles = get_roles() 162 | 163 | -- Add default authenticated role 164 | if _SESSION.user_id == user_id then 165 | user_roles.authenticated = 'authenticated' 166 | else 167 | user_roles.anonymous = 'anonymous' 168 | end 169 | 170 | -- Traverse config.user_role to users_roles 171 | for _, role_id in pairs(config.user_role[user_id] or {}) do 172 | if roles[role_id] then 173 | user_roles[role_id] = role_id 174 | end 175 | end 176 | 177 | -- Load user <--> role relationships from database storage 178 | if config.permissions_storage then 179 | local rs, err = db_query([[ 180 | SELECT ur.role_id 181 | FROM user_role ur JOIN role r ON ur.role_id = r.id 182 | WHERE user_id = ?]], user_id) 183 | for row in rs:rows(true) do 184 | user_roles[row.role_id] = row.role_id 185 | end 186 | end 187 | 188 | users_roles[user_id] = user_roles 189 | end 190 | 191 | return users_roles[user_id] 192 | end 193 | end 194 | 195 | do 196 | local users_permissions = {} 197 | 198 | --[[ Load user permissions from roles in provided account object. 199 | ]] 200 | function get_user_permissions(user_id, reset) 201 | if nil == users_permissions[user_id] or reset then 202 | local permissions = {} 203 | local user_roles = get_user_roles(user_id) 204 | if nil == config.permissions then config.permissions = {} end 205 | 206 | -- Load permissions from settings 207 | for role_id, assigned in pairs(user_roles or {}) do 208 | for _, perm in pairs(config.permissions[role_id] or {}) do 209 | permissions[perm] = true 210 | end 211 | end 212 | 213 | -- Load permissions from database storage 214 | if config.permissions_storage then 215 | local roles = xtable(get_user_roles(user_id) or {}) 216 | 217 | local rs, err = db_query([[ 218 | SELECT permission 219 | FROM role_permission 220 | WHERE role_id IN (']] .. roles:concat("', '") .. [[') 221 | GROUP BY permission 222 | ORDER BY permission 223 | ]]) 224 | for row in rs:rows(true) do 225 | if nil == permissions[row.permission] then 226 | permissions[row.permission] = true 227 | end 228 | end 229 | end 230 | 231 | users_permissions[user_id] = permissions 232 | end 233 | 234 | return users_permissions[user_id] 235 | end 236 | end 237 | 238 | function access(perm, user_id) 239 | if nil == user_id then user_id = _SESSION.user_id end 240 | local account = load(user_id) 241 | 242 | local permissions = get_user_permissions(user_id) 243 | 244 | if tonumber(user_id) == 1 then 245 | return true 246 | elseif not empty(permissions) then 247 | return permissions[perm] or false 248 | end 249 | 250 | return false 251 | end 252 | 253 | function default_page() 254 | if not is_logged_in() then 255 | goto 'user/login' 256 | end 257 | end 258 | 259 | function login_page() 260 | add_js 'libraries/jquery.min.js' 261 | add_js 'modules/user/user_login.js' 262 | 263 | return theme{'form', attributes = {id = 'login_form'}, 264 | elements = { 265 | {'textfield', title = 'Username', value = '', attributes = {id = 'login_user'}}, 266 | {'textfield', title = 'Password', value = '', attributes = {id = 'login_pass', type = 'password'}}, 267 | {'submit', value = 'Login', attributes = {id = 'login_submit'}}, 268 | }, 269 | } 270 | end 271 | 272 | function logout_page() 273 | if is_logged_in then 274 | session_destroy() 275 | goto '' 276 | end 277 | end 278 | 279 | function create(entity) 280 | local rs, err 281 | 282 | if entity.type == nil then entity.type = 'user' end 283 | 284 | if entity.id then 285 | rs, err = db_query([[ 286 | INSERT INTO users(id, name, mail, pass, active, created) 287 | VALUES(?, ?, ?, ?, ?, ?)]], 288 | entity.id, 289 | entity.name, 290 | entity.mail, 291 | entity.pass, 292 | entity.active or false, 293 | entity.created or time() 294 | ) 295 | else 296 | rs, err = db_query([[ 297 | INSERT INTO users(name, mail, pass, active, created) 298 | VALUES(?, ?, ?, ?, ?)]], 299 | entity.name, 300 | entity.mail, 301 | entity.pass, 302 | entity.active or false, 303 | entity.created or time() 304 | ) 305 | entity.id = db_last_insert_id('users', 'id') 306 | end 307 | 308 | if not err then 309 | module_invoke_all('entity_after_save', entity) 310 | end 311 | return entity.id, err 312 | end 313 | 314 | function update(entity) 315 | local rs, err 316 | rs, err = db_query('UPDATE content SET name = ?, mail = ?, pass = ?, active = ?, created = ? WHERE id = ?', 317 | entity.name, 318 | entity.mail, 319 | entity.pass, 320 | entity.active, 321 | entity.created, 322 | entity.id 323 | ) 324 | if not err then 325 | module_invoke_all('entity_after_save', entity) 326 | end 327 | return rs, err 328 | end 329 | 330 | 331 | function auth_service() 332 | local input, parsed, pos, err, account 333 | local output = {authenticated = false} 334 | 335 | input = request_get_body() 336 | 337 | if input == nil then 338 | output.authenticated = is_logged_in() 339 | return output 340 | end 341 | 342 | parsed, pos, err = json.decode(input, 1, nil) 343 | 344 | if err then 345 | error(err) 346 | elseif 347 | 'table' == type(parsed) and not empty(parsed.user) and 348 | not empty(parsed.pass) 349 | then 350 | account = load_by_field('name', parsed.user) 351 | if 'table' == type(account) and not empty(account.id) then 352 | if account.pass == hash(config.algorithm or 'sha256', parsed.pass or '') then 353 | output.authenticated = true 354 | module_invoke_all('user_login', account, output) 355 | _SESSION.user_id = account.id 356 | 357 | if _GET.redirect and url_parse(_GET.redirect).host == _SERVER 'HTTP_HOST' then 358 | output.redirect = _GET.redirect 359 | end 360 | end 361 | end 362 | end 363 | 364 | return output 365 | end 366 | 367 | --[[ Return the current user from _SESSION. 368 | ]] 369 | function current() 370 | return load(_SESSION.user_id) 371 | end 372 | 373 | --[[ Render author. 374 | ]] 375 | function theme.author(variables) 376 | local entity = variables.entity or {} 377 | local account = load(entity.user_id) 378 | 379 | if empty(account.id) then 380 | return account.name 381 | else 382 | return l(account.name, 'user/' .. account.id) 383 | end 384 | end 385 | -------------------------------------------------------------------------------- /modules/user/user_login.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | function login_request() { 4 | var user = $('#login_form #login_user').val(); 5 | var pass = $('#login_form #login_pass').val(); 6 | var hash; 7 | /* Authenticate */ 8 | $.ajax({ 9 | type: 'POST', 10 | url: '/user/auth', 11 | contentType: 'application/json; charset=utf-8', 12 | data: JSON.stringify({user: user, pass: pass}), 13 | dataType: 'json', 14 | processData: false, 15 | success: function(data) { 16 | if (data.authenticated) { 17 | window.location = '/'; 18 | } 19 | else { 20 | alert('Login error! Please check your credentials.'); 21 | } 22 | }, 23 | error: function() { 24 | alert('Authentication error. Please try again later.'); 25 | } 26 | }); 27 | } 28 | 29 | $(document).ready(function() { 30 | $('#login_form #login_submit').click(function() { 31 | try { 32 | login_request(); 33 | } finally { 34 | /* Prevent browser to send POST request, since we already did it */ 35 | return false; 36 | } 37 | }); 38 | }); 39 | 40 | })(jQuery); 41 | -------------------------------------------------------------------------------- /nginx.ophal.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name example.com; 4 | 5 | root /path/to/document/root/; 6 | 7 | location = / { 8 | index index.cgi; 9 | } 10 | 11 | location / { 12 | index index.cgi; 13 | 14 | if (!-f $request_filename) { 15 | rewrite ^(.*)$ /index.cgi last; 16 | break; 17 | } 18 | 19 | if (!-d $request_filename) { 20 | rewrite ^(.*)$ /index.cgi last; 21 | break; 22 | } 23 | } 24 | 25 | error_page 404 /index.cgi; 26 | 27 | ## All static files will be served directly. 28 | location ~* ^.+\.(?:css|cur|js|jpg|jpeg|gif|htc|ico|png|html|xml|less|ttf|pdf|map)$ { 29 | access_log off; 30 | expires 30d; 31 | ## No need to bleed constant updates. Send the all shebang in one 32 | ## fell swoop. 33 | tcp_nodelay off; 34 | ## Set the OS file cache. 35 | open_file_cache max=3000 inactive=120s; 36 | open_file_cache_valid 45s; 37 | open_file_cache_min_uses 2; 38 | open_file_cache_errors off; 39 | } 40 | 41 | location ~ .cgi$ { 42 | lua_code_cache off; 43 | default_type text/html; 44 | 45 | ## Make sure to run Ophal on its document root 46 | set_by_lua $_ ' 47 | require "lfs".chdir(ngx.var.document_root) 48 | return nil 49 | '; 50 | 51 | content_by_lua_file $request_filename; 52 | } 53 | 54 | access_log /var/log/nginx/example.com-access.log; 55 | error_log /var/log/nginx/example.com-error.log; 56 | } 57 | 58 | -------------------------------------------------------------------------------- /themes/basic/footer.tpl.html: -------------------------------------------------------------------------------- 1 |

    Footer

    2 | Aliquam imperdiet luctus placerat. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; 3 |
    Powered by .
    4 | -------------------------------------------------------------------------------- /themes/basic/html.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | > 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <?lua print(header_title) ?> 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 |
    22 |
    23 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /themes/basic/images/ophal.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ophal/core/85269272c945635e3478d716c95032308b5f360d/themes/basic/images/ophal.ico -------------------------------------------------------------------------------- /themes/basic/images/ophalproject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ophal/core/85269272c945635e3478d716c95032308b5f360d/themes/basic/images/ophalproject.png -------------------------------------------------------------------------------- /themes/basic/sidebar_first.tpl.html: -------------------------------------------------------------------------------- 1 |

    Sidebar first

    2 | Phasellus a massa nisl, quis auctor metus. Praesent sodales, sapien non pharetra facilisis, eros magna iaculis dui, et accumsan augue dui non massa. Aliquam tempor nisi bibendum orci aliquet eget molestie ante ullamcorper. Nullam interdum magna id metus commodo vel congue sem porta. 3 | -------------------------------------------------------------------------------- /themes/basic/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | } 3 | 4 | #header { 5 | position: absolute; 6 | top: 0; 7 | } 8 | #header #logo { 9 | float: left; 10 | } 11 | #header .title { 12 | float: right; 13 | } 14 | 15 | #sidebar { 16 | width: 160px; 17 | float: left; 18 | } 19 | 20 | #messages div { 21 | margin: 4em 0 -4em 0; 22 | padding: 1em; 23 | } 24 | #messages .error-message { 25 | background-color: lightyellow; 26 | color: red; 27 | } 28 | 29 | #content { 30 | margin-top: 80px; 31 | margin-left: 170px; /* left sidebar*/ 32 | } 33 | 34 | h1 { 35 | margin-bottom: 5px; 36 | } 37 | 38 | #footer { 39 | clear: both; 40 | } 41 | 42 | .powered-by { 43 | margin: 10px 0; 44 | font-size: 12px; 45 | text-align: right; 46 | } 47 | -------------------------------------------------------------------------------- /themes/install/footer.tpl.html: -------------------------------------------------------------------------------- 1 |

    Do you have issues? Please contribute by reporting any issues to the Ophal documentation tracker.

    2 |
    Powered by .
    3 | -------------------------------------------------------------------------------- /themes/install/html.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | > 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <?lua print(header_title) ?> 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 |
    22 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /themes/install/images/ophal.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ophal/core/85269272c945635e3478d716c95032308b5f360d/themes/install/images/ophal.ico -------------------------------------------------------------------------------- /themes/install/images/ophalproject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ophal/core/85269272c945635e3478d716c95032308b5f360d/themes/install/images/ophalproject.png -------------------------------------------------------------------------------- /themes/install/sidebar_first.tpl.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ophal/core/85269272c945635e3478d716c95032308b5f360d/themes/install/sidebar_first.tpl.html -------------------------------------------------------------------------------- /themes/install/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "lucida grande",lucida,tahoma,helvetica,arial,sans-serif; 3 | margin-top: 0; 4 | padding-top: 0; 5 | } 6 | 7 | #header { 8 | position: absolute; 9 | top: 0; 10 | } 11 | #header #logo { 12 | float: left; 13 | } 14 | #header .title { 15 | float: right; 16 | } 17 | 18 | #sidebar { 19 | width: 160px; 20 | float: left; 21 | } 22 | 23 | #content { 24 | margin-top: 80px; 25 | margin-left: 170px; /* left sidebar*/ 26 | font-size: 90%; 27 | } 28 | 29 | h1 { 30 | margin-bottom: 5px; 31 | } 32 | 33 | #footer { 34 | clear: both; 35 | } 36 | 37 | .powered-by { 38 | margin: 10px 0; 39 | font-size: 12px; 40 | text-align: right; 41 | } 42 | 43 | table th { 44 | text-align: left; 45 | } 46 | 47 | p {} -------------------------------------------------------------------------------- /themes/mobile/footer.tpl.html: -------------------------------------------------------------------------------- 1 |
    Powered by .
    2 | -------------------------------------------------------------------------------- /themes/mobile/html.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | > 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <?lua print(header_title) ?> - Mobile 11 | 12 | 13 | 14 | 42 | 43 | 44 | 45 | 49 |
    50 | 51 | 52 |
    53 |
    54 |

    Test to check the mobile detection feature.

    55 |
    56 | 65 |

    This is . Your UA is

    66 |
    67 | 68 |
    69 |
    70 |

    Supported methods tests

    71 |
    72 | 73 | 76 | 77 | 78 | class="true"> 79 | 80 | 81 |
    is()
    82 |
    83 | 84 |
    85 |
    86 |

    Other tests

    87 |
    88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 |
    isiphone()
    isIphone()
    isTablet()
    isIOS()
    isWhateverYouWant()
    111 |
    112 | 113 |
    114 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /themes/mobile/images/ophal.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ophal/core/85269272c945635e3478d716c95032308b5f360d/themes/mobile/images/ophal.ico -------------------------------------------------------------------------------- /themes/mobile/images/ophalproject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ophal/core/85269272c945635e3478d716c95032308b5f360d/themes/mobile/images/ophalproject.png -------------------------------------------------------------------------------- /themes/mobile/style.css: -------------------------------------------------------------------------------- 1 | 2 | #content { 3 | margin-top: 40px; 4 | } 5 | 6 | h1 { 7 | margin-bottom: 5px; 8 | } 9 | 10 | .powered-by { 11 | margin: 10px 0; 12 | font-size: 12px; 13 | text-align: right; 14 | } 15 | --------------------------------------------------------------------------------