├── .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 (''):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 = '%s: %s '
33 | local row_nl = '%s '
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, '
')
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 (' %s:'):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 ('%s '):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 ('%s '):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 | ('
'
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 | For enhanced security, do *not* run this installer in production.
74 | Javascript enabled is needed in order to complete the installation.
75 | No dabatase is created by this installer, you need to create one in advance.
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 | 'Library Machine name Required? Status Error '
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, ('%s '):format(library.name))
151 | tinsert(output, ('"%s" '):format(machine_name))
152 | tinsert(output, ('%s '):format(library.required and 'Required' or 'Optional'))
153 | -- Status
154 | status, err = pcall(require, machine_name)
155 | if status then
156 | tinsert(output, 'Found ')
157 | else
158 | continue = false
159 | tinsert(output, ('Missing "%s" '):format(err))
160 | end
161 | tinsert(output, ' ')
162 | end
163 | tinsert(output, '
')
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 | [=[
236 |
Step 2. Create file settings.lua
237 |
Copy the following text into the file settings.lua and put it right in the exact same folder of file index.cgi :
238 |
]=],
368 | '
',
369 | [=[
370 |
Step 3. Create file vault.lua
371 |
Copy the following text into the file vault.lua and put it right in the exact same folder of file index.cgi :
372 |
]=],
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 | $('')
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 |
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 |
Submitted by on
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/modules/content/content_teaser.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 | (''
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 |
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 |
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 | - Mobile
11 |
12 |
13 |
14 |
42 |
43 |
44 |
45 |
49 |
50 |
51 |
52 |
53 |
56 |
65 | This is . Your UA is
66 |
67 |
68 |
69 |
70 | Supported methods tests
71 |
72 |
73 |
76 |
77 | is()
78 | class="true">
79 |
80 |
81 |
82 |
83 |
84 |
85 |
88 |
89 |
90 |
91 | isiphone()
92 |
93 |
94 |
95 | isIphone()
96 |
97 |
98 |
99 | isTablet()
100 |
101 |
102 |
103 | isIOS()
104 |
105 |
106 |
107 | isWhateverYouWant()
108 |
109 |
110 |
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 |
--------------------------------------------------------------------------------