diff --git a/.gitignore b/.gitignore index 2c05944a..7931343e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ node_modules/* config.yaml config.yml data/* +.idea +__pycache__/ +Dockerfile +.dockerignore +package-lock.json diff --git a/README_FORK.md b/README_FORK.md new file mode 100644 index 00000000..c582bd1b --- /dev/null +++ b/README_FORK.md @@ -0,0 +1,106 @@ +Intro +----- +This is a fork of Jingo (at the time of v1.8.5). Modifications to the source code have been made in order to add a number of features to Jingo and fix a couple bugs. + +Merge Status: __pending__ + +ChangeLog +--------- +### List of Modifications +- fixed bug in gitExec when repo path contains one or more spaces +- fixed bug with markMissingPagesAsAbsent request for pages with ' in the name +- added redirection of synonyms to official page toggled in config +- added sidebar, main and footer column width customization to config +- added options to use regexp to redact exerpts of information for anonymous users to config +- added option to redact commit messages for anonymous users to config +- added option to redact entire pages from search and list for anonymous users to config +- added option to include initial paragraph in page list +- added media folder to static routes on page +- added override of standard favicon with any favicon in root of media folder +- added option to serve files from local version rather than CDN +- added option to disable case sensitivity in page links and aliases +- added option to lift block elements outside of paragraphs during markdown rendering + +### Mod Strategy +All changes and/or additions have been marked with comments which begin with "// MOD" so as to make it easier to hunt down changes. Existing code has been preserved whenever possible through the use of configuration flags to enable the additional/optional functionality of this fork. Also, additional methods and modules have been employed using naming conventions so as to least possibly conflict with future changes to the main branch. + +Additional Configuration Options +-------------------------------- + +#### application.mediaSubdir (string: "") + + If you have media files you would like to host locally, such as a favicon, logo, images or css or js dependencies, inside a directory of the repository, specify its name here. + +#### application.serveLocal (boolean: false) + + With this option, you can use a local copy of a resource instead of a CDN. Currently, this only applies to the Ubuntu font families requested in the head of each page. + +#### application.percolateBlocks (string: "div, blockquote") + + With this option, you can add other block elements to the list of block elements which are lifted out of their location in paragraphs during markdown rendering. Separate each element tag type you would like to be placed outside of paragraphs with a comma (and optional space). Without this option, only table blocks are lifted outside of paragraphs. + +#### features.pageSummaries (boolean: true) + + With this option, the html rendered version of the first paragraph of each page will be included its listing when a user browses all the pages. + +#### features.caseSensitive (boolean: false) + + With this option, you can force aliases and page linking to only match a page if the cases match. For example, if you want to have any link to "Introduction" to redirect to Home.md, when this option is disabled, links to "introduction" would also redirect to Home.md. + +#### assets.css (string: '') + + With the assets.css string, you can add the file path to any number of css sheets to be imported on page rendering in the \
element prior to the customization style. In this way, you can add dependencies to the customization style. Separate stylesheets should be delineated with a comma and all such sheets must be located in the mediaSubdir folder or they will not be imported. + +#### assets.js (string: '') + + With the assets.js string, you can add the file path to any number of js files to be appended to the bottom of the \ element on page rendering prior to the customization script. In this way, you can add local dependencies to the customization script. Separate javascript files should be delineated with a comma and all such files must be located in the mediaSubdir folder or they will not be imported. + +#### aliases (map) + + If you would like to setup one or more aliases for a wiki entry, which will redirect to that page, you can include each alias and its associated page as a key:value pair in this map. Whenever a page request to an alias value is made, the server will return the page it is associated with and also include a line underneath the title of the page that indicates that the requested term redirects to this page. Since URLs cannot contain a space, all aliases with a word break need to replace that word break with a '-'. Whereas the alias should not contain .md, the page it redirects to must include .md in its name. + +#### layout.sidebarWidth (integer: 2) + + With this layout field, you can change the width of the sidebar content in desktop view between 0 and 10. The default width for Jingo is 2. The sidebarWidth + mainWidth (see below) cannot exceed 12. + +#### layout.mainWidth (integer: 8) + + With this layout field, you can change the width of the main page in desktop view between 2 and 12. The default width for Jingo is 8. The sidebarWidth (see above) + mainWidth cannot exceed 12. + +#### layout.footerWidth (integer: 8) + + With this layout field, you can change the width of the footer content in desktop view between 0 and 12. The default width for Jingo is 8. + +#### layout.sidebarMobile (boolean: true) + + With this layout field, you can disable the sidebar content (from appearing on top of the page) when viewed from mobile devices. + +#### redaction.enabled (boolean: false) + + When redaction is enabled, it is possible to remove certain content from the view generated for anonymous users. Redaction also removes the commit records and history pages from all content rendered for anonymous users. A number of different techniques are provided for redacting content (see below) using regular expression syntax. When redaction is enabled, all pages and searches will look for content which matches the regular expressions provided and remove it from anonymous views. A global, multi-line search is the default option for all regular expression patterns, so if matching new lines is important, they must be included in the regular expression. If any part of a redaction regular expression is grouped, then all parts that are not inside a grouping will be redacted for both anonymous and authenticated users. Regular expression occurs before pages go through HTML rendering and so will match the markdown as it appears for each entry. + +#### redaction.hiddenPage (string: "^") + + If any part of the document matches the regexp in hidden page, then the entire page will be removed from view from anonymous users and will return a 404 page error. In addition, the page will not show up in any search or list function. This is the only redaction expression that is not global, as it only requires one match to trigger. + +#### redaction.privateContent (string: "([\s\S]*?)") + + Any sections of the document which match a privateContent regexp will be redacted from the document. Content inside the redaction will not show up in any search result. If no grouping is found in the regexp, then the entire regexp will be returned for the authenticated user. Otherwise, only the content inside groupings will be returned to the authenticated user. + +#### redaction.futureContent (string: "([\s\S]*?)") + + Any section of the document which matches an futureContent regexp will be tested to see if it should be redacted from anonymous users. This technique will construct a date from the first (4 to 10)integers it finds inside the matched content using the following order: YYYYMMDDHH and then see if that date exists beyond the current date. If the date it finds is in the future, it will redact the content from anonymous users. So, for example, an excerpt such as Future stuff will be treated as content not to be revealed until on or after 20990909. If the date is in the past, it will reveal the content it finds in either the first group (if there is only one group in the regexp) or the second group (if there are two or more groupings). This technique requires at least one grouping to be specified in the regexp or it will have no effect. + +#### redaction.sectionContent[0].expression (string: "([\s\S]*?)") + + Any section of the document which matches an expression in the list of sectionContent will be tested to see if it should be redacted from anonymous users. This technique will look for the first group of integers it finds inside each match and compare that integer to the one provided in the associated current value. If the integer it finds is greater than the current value, the section will be redacted from anonymous users. So, for example, an excerpt such as A late chapter would be redacted if the current chapter (current) is 9. Like the futureContent technique, if the current value is equal to or greater than the integer found, it will reveal the content it finds in either the first group (if there is only one group in the regexp) or the second group (if there are two or more groupings). This technique requires at least one grouping to be specified in the regexp or it will have no effect. + +#### redaction.sectionContent[0].current (integer: 0) + + This field is included in order to evaluate the highest value in an associated sectionContent expression which is public. Any value that is found to be higher than the current value will be redacted from anonymous users. + + + + + + diff --git a/jingo b/jingo index dc0f9589..600ac5e3 100755 --- a/jingo +++ b/jingo @@ -55,6 +55,7 @@ var refspec = config.get('application').remote.split(/\s+/) Git.setup(config.get('application').git, config.get('application').repository, config.get('application').docSubdir, + config.get('application').mediaSubdir, // MOD add media subdirectory in config refspec, function (err, version) { if (err) { console.log(err) diff --git a/lib/app.js b/lib/app.js index f60304b0..ac97ee8d 100644 --- a/lib/app.js +++ b/lib/app.js @@ -24,6 +24,7 @@ var gravatar = require('gravatar') var passport = require('passport') var methodOverride = require('method-override') var flash = require('express-flash') +var fs = require('fs') // MOD import dependency for media folder mapping var app @@ -47,6 +48,30 @@ module.exports.initialize = function (config) { app.locals.baseUrl = config.get('server').baseUrl } + // MOD add assets to locals + app.locals.assets = { + css: [], + js: [] + } + var cssArray = config.get('assets').css.split(',') + for (var i = 0; i < cssArray.length; i++){ + if (cssArray[i].trim() && fs.existsSync(Git.mediaPath(cssArray[i].trim()))){ + app.locals.assets.css.push(cssArray[i].trim()) + } + } + var jsArray = config.get('assets').js.split(',') + for (var i = 0; i < jsArray.length; i++){ + if (jsArray[i].trim() && fs.existsSync(Git.mediaPath(jsArray[i].trim()))){ + app.locals.assets.js.push(jsArray[i].trim()) + } + } + if (app.locals.assets.css){ + console.log('Adding styles ' + JSON.stringify(app.locals.assets.css)) + } + if (app.locals.assets.js){ + console.log('Adding scripts ' + JSON.stringify(app.locals.assets.js)) + } + // View helpers app.use(function (req, res, next) { res.locals = { @@ -84,6 +109,43 @@ module.exports.initialize = function (config) { } return config.get('application').favicon.trim() }, + // MOD add layout configuration to locals + get sidebarWidth () { + return config.get('layout').sidebarWidth.toString() + }, + get mainWidth () { + return config.get('layout').mainWidth.toString() + }, + get footerWidth () { + return config.get('layout').footerWidth.toString() + }, + get footerSidebarWidth () { + return (12 - config.get('layout').footerWidth).toString() + }, + get sidebarMobile () { + return config.get('layout').sidebarMobile + }, + // MOD add redaction and page summary toggles + get redactEnabled () { + return config.get('redaction').enabled + }, + get redactContent () { + return config.get('redaction').enabled && !req.user + }, + get pageSummaries () { + return config.get('features').pageSummaries + }, + // MOD add serve local toggle + get serveLocal () { + return config.get('application').serveLocal + }, + // MOD add asset imports + get cssAssets () { + return app.locals.assets.css + }, + get jsAssets () { + return app.locals.assets.js + }, isAnonymous: function () { return !req.user @@ -143,7 +205,10 @@ module.exports.initialize = function (config) { return method } })) - + // MOD add files in media path to static files + if (Git.mediaPath()) { + app.use(express.static(Git.mediaPath())) + } app.use(express.static(path.join(__dirname + '/../', 'public'))) // eslint-disable-line no-path-concat app.use(cookieParser()) app.use(cookieSession({ @@ -152,11 +217,11 @@ module.exports.initialize = function (config) { cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 } })) app.use(session({ name: 'jingosid', - secret: config.get('application').secret, - cookie: { httpOnly: true }, - saveUninitialized: true, - resave: true - })) + secret: config.get('application').secret, + cookie: { httpOnly: true }, + saveUninitialized: true, + resave: true + })) app.use(flash()) app.use(expValidator()) @@ -173,7 +238,7 @@ module.exports.initialize = function (config) { if (/^\/auth\//.test(req.url) || /^\/misc\//.test(req.url) || (/^\/login/.test(req.url) && !config.get('authorization').anonRead) - ) { + ) { return next() } @@ -212,10 +277,10 @@ module.exports.initialize = function (config) { app.use('/wiki', wikiStatic.configure()) app.use(require('../routes/wiki')) - .use(require('../routes/pages')) - .use(require('../routes/search')) - .use(require('../routes/auth')) - .use(require('../routes/misc')) + .use(require('../routes/pages')) + .use(require('../routes/search')) + .use(require('../routes/auth')) + .use(require('../routes/misc')) // Server error app.use(function (err, req, res, next) { diff --git a/lib/config.js b/lib/config.js index 23a97061..b329258a 100644 --- a/lib/config.js +++ b/lib/config.js @@ -29,10 +29,20 @@ module.exports = (function () { if (typeof config[keys[i]] === 'undefined') { continue } - aliens = _.difference(Object.keys(config[keys[i]]), Object.keys(this.defaults[keys[i]])) - if (aliens.length > 0) { - error = 'Unrecognized configuration option(s) ' + aliens.join(',') + ' in section ' + keys[i] - return false + // MOD Allow unlimited keys in aliases section as long as they have string values + if (keys[i] === 'aliases') { + for (var k in config['aliases']) { + if (typeof config['aliases'][k] !== 'string') { + error = 'Value for configuration field ' + k + ' in section aliases must be a string.' + return false + } + } + } else { + aliens = _.difference(Object.keys(config[keys[i]]), Object.keys(this.defaults[keys[i]])) + if (aliens.length > 0) { + error = 'Unrecognized configuration option(s) ' + aliens.join(',') + ' in section ' + keys[i] + return false + } } } @@ -50,7 +60,10 @@ module.exports = (function () { logo: '', favicon: '', repository: '', + extension: '', docSubdir: '', + mediaSubdir: '', // MOD name of sub-directory to use for additional public accessible media + serveLocal: false, // MOD enable serving of files from local server rather than a CDN remote: '', pushInterval: 30, secret: 'change me', @@ -59,6 +72,7 @@ module.exports = (function () { loggingMode: 1, pedanticMarkdown: true, gfmBreaks: true, + percolateBlocks: 'div, blockquote', // MOD html block element tags to lift out of paragraphs staticWhitelist: '/\\.png$/i, /\\.jpg$/i, /\\.gif$/i', proxyPath: '' }, @@ -105,7 +119,9 @@ module.exports = (function () { features: { markitup: false, codemirror: true, - gravatar: true + gravatar: true, + pageSummaries: true, // MOD enable initial paragraph to be returned in list of documents + caseSensitive: false // MOD enable case sensitivity in redirection }, server: { @@ -152,12 +168,43 @@ module.exports = (function () { footer: '_footer.md', style: '_style.css', script: '_script.js' + }, + + // MOD comma delineated strings of css and js files to include in assets on page load + assets: { + css: '', + js: '' + }, + + // MOD map of alias terms and the page each redirects to + aliases: { + introduction: 'Home.md' + }, + + // MOD configuration of columns in layout.pug + layout: { + sidebarWidth: 2, + mainWidth: 8, + footerWidth: 8, + sidebarMobile: true + }, + + // MOD various types of RegExp to use for redaction of content + redaction: { + enabled: false, + hiddenPage: '^', + privateContent: '', + futureContent: '([\\s\\S]*?)', + sectionContent: [{ + expression: '([\\s\\S]*?)', + current: 0 + }] } + }, // Ensure that all the key will have a sane default value validate: function () { - config.application = _.extend({}, this.defaults.application, config.application) config.application.pushInterval = (parseInt(config.application.pushInterval, 10) | 0) @@ -243,6 +290,75 @@ module.exports = (function () { config.customizations = _.extend({}, this.defaults.customizations, config.customizations) + // MOD extend and validate assets + config.assets = _.extend({}, this.defaults.assets, config.assets) + var import_assets = config.assets.css + if (import_assets){ + import_assets += ', ' + } + import_assets += config.assets.js + if (import_assets && !config.application.mediaSubdir){ + error = 'Use of assets ' + import_assets + ' requires the specification of a mediaSubdir.' + return false + } + + // MOD validate layout fields + config.layout = _.extend({}, this.defaults.layout, config.layout) + config.layout.sidebarWidth = parseInt(config.layout.sidebarWidth, 10) + config.layout.sidebarWidth = config.layout.sidebarWidth < 11 ? config.layout.sidebarWidth : 3 + config.layout.sidebarWidth = config.layout.sidebarWidth >= 0 ? config.layout.sidebarWidth : 3 + config.layout.mainWidth = parseInt(config.layout.mainWidth, 10) + config.layout.mainWidth = config.layout.mainWidth < 13 ? config.layout.mainWidth : 9 + config.layout.mainWidth = config.layout.mainWidth > 1 ? config.layout.mainWidth : 9 + if (config.layout.sidebarWidth + config.layout.mainWidth > 12) { + error = 'Total width of sidebarWidth + mainWidth must not exceed 12.' + return false + } + config.layout.footerWidth = parseInt(config.layout.footerWidth, 10) + config.layout.footerWidth = config.layout.footerWidth < 13 ? config.layout.footerWidth : 9 + config.layout.footerWidth = config.layout.footerWidth > 0 ? config.layout.footerWidth : 9 + + // MOD extend aliases + config.aliases = _.extend({}, this.defaults.aliases, config.aliases) + + // MOD validate redaction fields + config.redaction = _.extend({}, this.defaults.redaction, config.redaction) + var regexFields = [ 'hiddenPage', 'privateContent', 'futureContent' ] + for (var i = 0; i < regexFields.length; i++) { + var regexValue = config.redaction[regexFields[i]] + if (regexValue) { + try { + var testRegex = RegExp(regexValue) + } catch (e) { + error = 'Value for configuration field ' + regexFields[i] + ' in section redaction must be a valid RegExp string.' + return false + } + } + } + // MOD validate sequential section fields + if (config.redaction.sectionContent) { + for (var i = 0; i < config.redaction.sectionContent.length; i++) { + var latestSection = config.redaction.sectionContent[i] + var errorPrefix = 'Value for configuration field sectionContent item [' + i.toString() + '] key expression in section redaction' + if (latestSection.expression) { + try { + var testRegex = RegExp(latestSection.expression) + } catch (e) { + error = errorPrefix + ' must be a valid RegExp string.' + return false + } + if (!(/\\d\+/.test(latestSection.expression))) { + error = errorPrefix + ' must contain the term \\d+.' + return false + } + if (typeof (latestSection.current) !== 'number') { + error = errorPrefix + ' requires a current be included.' + return false + } + } + } + } + return true }, diff --git a/lib/gitmech.js b/lib/gitmech.js index 9c6606be..76f6e7d7 100644 --- a/lib/gitmech.js +++ b/lib/gitmech.js @@ -4,6 +4,8 @@ var semver = require('semver') var fs = require('fs') var gitCommands, workTree, docSubdir +var mediaSubdir // MOD add variable for media static content +var gitCommandsQuote // MOD add variable for quoting absolute paths var gitENOENT = /fatal: (Path "([^"]+)" does not exist in "([0-9a-f]{40})"|ambiguous argument "([^"]+)": unknown revision or path not in the working tree.)/ // Internal helper to talk to the git subprocess (spawn) @@ -51,6 +53,24 @@ function gitExec (commands, callback) { }) } +// MOD fixed gitExec function for concatenating paths with blank spaces +function gitExecFix (commands, callback) { + if (gitCommandsQuote) { + commands = gitMech.gitBin + ' ' + gitCommandsQuote.concat(commands).join(' ') + } else { + commands = gitMech.gitBin + ' ' + gitCommands.concat(commands).join(' ') + } + // There is a limit at 200KB (increase it with maxBuffer option) + childProcess.exec(commands, { cwd: workTree }, function (error, stdout, stderr) { + if (error || stderr.length > 0) { + error = new Error(commands + '\n' + stderr) + callback(error) + return + } + callback(null, stdout) + }) +} + function join (arr) { var result var index = 0 @@ -85,7 +105,7 @@ var gitMech = { remote: '', - setup: function (gitBin, repoDir, repoDocSubdir, refspec, callback) { + setup: function (gitBin, repoDir, repoDocSubdir, repoMediaSubdir, refspec, callback) { // MOD add media arg this.gitBin = gitBin || 'git' childProcess.exec(this.gitBin + ' --version', function (err, stdout, stderr) { @@ -136,10 +156,29 @@ var gitMech = { return } + // MOD add validation of media subdirectory + mediaSubdir = repoMediaSubdir.trim().replace(/^\/|\/$/g, '') + if (mediaSubdir !== '') { + mediaSubdir = mediaSubdir + '/' + } + // MOD report usage of media subdirectory + if (mediaSubdir) { + try { + fs.statSync(repoDir + '/' + mediaSubdir) + console.log('Using media subdirectory at ' + repoDir + '/' + mediaSubdir) + } catch (e) {} + } + try { var gitDir = path.join(repoDir, '.git') fs.statSync(gitDir) workTree = repoDir + // MOD construct alternate commands for paths with blank space + var quoteWorktree = '"' + workTree + '"' + var quoteGitdir = '"' + gitDir + '"' + if (/\s/.test(gitDir)) { + gitCommandsQuote = ['--git-dir=' + quoteGitdir, '--work-tree=' + quoteWorktree] + } gitCommands = ['--git-dir=' + gitDir, '--work-tree=' + workTree] } catch (e) { callback('Bad repository path (not initialized): ' + repoDir) @@ -159,6 +198,15 @@ var gitMech = { return workTree + '/' + docSubdir + path }, + // MOD add media path to file + mediaPath: function (path = '') { + if (mediaSubdir) { + return workTree + '/' + mediaSubdir + path + } else { + return null + } + }, + show: function (path, version, callback) { gitSpawn(['show', version + ':' + docSubdir + path], function (err, data) { if (err) { @@ -393,7 +441,8 @@ var gitMech = { }, ls: function (pattern, callback) { - gitExec([ 'ls-tree', '--name-only', '-r', 'HEAD', '--', docSubdir + pattern ], function (err, data) { + // MOD fix function to use quoted paths + gitExecFix([ 'ls-tree', '--name-only', '-r', 'HEAD', '--', docSubdir + pattern ], function (err, data) { if (err) { data = '' } diff --git a/lib/models.js b/lib/models.js index 7907ddc3..9924d962 100644 --- a/lib/models.js +++ b/lib/models.js @@ -67,17 +67,17 @@ Page.prototype.renameTo = function (newName) { } gitmech.mv(this.filename, - newFilename, - 'Page renamed (' + this.filename + ' => ' + newFilename + ')', - this.author, - function (err) { - if (err) { - reject() - } else { - this.setNames(newName) - resolve() - } - }.bind(this)) + newFilename, + 'Page renamed (' + this.filename + ' => ' + newFilename + ')', + this.author, + function (err) { + if (err) { + reject() + } else { + this.setNames(newName) + resolve() + } + }.bind(this)) }.bind(this)) } @@ -135,7 +135,6 @@ Page.urlFor = function (name, action, proxyPath) { var url = '' switch (true) { - case action === 'show': url = '/wiki/' + wname break @@ -245,14 +244,14 @@ Page.prototype.unlock = function (user) { Page.prototype.fetch = function (extended) { if (!extended) { return Promiserr.all([this.fetchContent(), - this.fetchMetadata(), - this.fetchHashes(1) - ]) + this.fetchMetadata(), + this.fetchHashes(1) + ]) } else { return Promiserr.all([this.fetchContent(), - this.fetchMetadata(), - this.fetchHashes(), - this.fetchLastCommitMessage()]) + this.fetchMetadata(), + this.fetchHashes(), + this.fetchLastCommitMessage()]) } } diff --git a/lib/namer.js b/lib/namer.js index 04d3bc90..5dc1b6c2 100644 --- a/lib/namer.js +++ b/lib/namer.js @@ -45,7 +45,7 @@ Namer.prototype.wikify = function (str) { return ret } - // Not symmetric by any chance, but still better than nothing +// Not symmetric by any chance, but still better than nothing Namer.prototype.unwikify = function (str) { var ret = str diff --git a/lib/redactor.js b/lib/redactor.js new file mode 100644 index 00000000..ee63f84c --- /dev/null +++ b/lib/redactor.js @@ -0,0 +1,187 @@ +#!/usr/bin/env node + +/* + * Jingo redactor module + * + * Copyright 2018 rcj1492"' + requested.replace(/-/g, ' ') + '" redirects here.
' + var headerMatch = /(^' + text + '
' + return text + +} + Marked.setOptions({ gfm: true, renderer: mdRenderer, @@ -140,7 +187,11 @@ var Renderer = { text = evalTags(text) text = applyDirectives(text) return Marked(text) - } + }, + + // MOD add redaction methods to renderer + redact: redactor.redact, + redirect: redactor.redirect } diff --git a/package.json b/package.json index cf96317a..b6408bbe 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "dependencies": { "bluebird": "^3.5.0", "body-parser": "^1.10.0", + "cheerio": "^1.0.0-rc.2", "commander": "^2.5.1", "cookie-parser": "^1.3.3", "cookie-session": "^1.1.0", diff --git a/public/css/google-fonts.css b/public/css/google-fonts.css new file mode 100644 index 00000000..a2b796d0 --- /dev/null +++ b/public/css/google-fonts.css @@ -0,0 +1,8 @@ +@font-face { + font-family: 'Ubuntu'; + src: url('/fonts/Ubuntu-Regular.ttf') format('truetype'); +} +@font-face { + font-family: 'Ubuntu Condensed'; + src: url('/fonts/UbuntuCondensed-Regular.ttf') format('truetype'); +} \ No newline at end of file diff --git a/public/fonts/Ubuntu UFL.txt b/public/fonts/Ubuntu UFL.txt new file mode 100644 index 00000000..ae78a8f9 --- /dev/null +++ b/public/fonts/Ubuntu UFL.txt @@ -0,0 +1,96 @@ +------------------------------- +UBUNTU FONT LICENCE Version 1.0 +------------------------------- + +PREAMBLE +This licence allows the licensed fonts to be used, studied, modified and +redistributed freely. The fonts, including any derivative works, can be +bundled, embedded, and redistributed provided the terms of this licence +are met. The fonts and derivatives, however, cannot be released under +any other licence. The requirement for fonts to remain under this +licence does not require any document created using the fonts or their +derivatives to be published under this licence, as long as the primary +purpose of the document is not to be a vehicle for the distribution of +the fonts. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this licence and clearly marked as such. This may +include source files, build scripts and documentation. + +"Original Version" refers to the collection of Font Software components +as received under this licence. + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to +a new environment. + +"Copyright Holder(s)" refers to all individuals and companies who have a +copyright ownership of the Font Software. + +"Substantially Changed" refers to Modified Versions which can be easily +identified as dissimilar to the Font Software by users of the Font +Software comparing the Original Version with the Modified Version. + +To "Propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification and with or without charging +a redistribution fee), making available to the public, and in some +countries other activities as well. + +PERMISSION & CONDITIONS +This licence does not grant any rights under trademark law and all such +rights are reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to propagate the Font Software, subject to +the below conditions: + +1) Each copy of the Font Software must contain the above copyright +notice and this licence. These can be included either as stand-alone +text files, human-readable headers or in the appropriate machine- +readable metadata fields within text or binary files as long as those +fields can be easily viewed by the user. + +2) The font name complies with the following: +(a) The Original Version must retain its name, unmodified. +(b) Modified Versions which are Substantially Changed must be renamed to +avoid use of the name of the Original Version or similar names entirely. +(c) Modified Versions which are not Substantially Changed must be +renamed to both (i) retain the name of the Original Version and (ii) add +additional naming elements to distinguish the Modified Version from the +Original Version. The name of such Modified Versions must be the name of +the Original Version, with "derivative X" where X represents the name of +the new work, appended to that name. + +3) The name(s) of the Copyright Holder(s) and any contributor to the +Font Software shall not be used to promote, endorse or advertise any +Modified Version, except (i) as required by this licence, (ii) to +acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with +their explicit written permission. + +4) The Font Software, modified or unmodified, in part or in whole, must +be distributed entirely under this licence, and must not be distributed +under any other licence. The requirement for fonts to remain under this +licence does not affect any document created using the Font Software, +except any version of the Font Software extracted from a document +created using the Font Software may only be distributed under this +licence. + +TERMINATION +This licence becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/Ubuntu-Regular.ttf b/public/fonts/Ubuntu-Regular.ttf new file mode 100644 index 00000000..2001d6ec Binary files /dev/null and b/public/fonts/Ubuntu-Regular.ttf differ diff --git a/public/fonts/UbuntuCondensed-Regular.ttf b/public/fonts/UbuntuCondensed-Regular.ttf new file mode 100644 index 00000000..602a3ee4 Binary files /dev/null and b/public/fonts/UbuntuCondensed-Regular.ttf differ diff --git a/public/fonts/glyphicons-halflings-regular.eot b/public/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 00000000..b93a4953 Binary files /dev/null and b/public/fonts/glyphicons-halflings-regular.eot differ diff --git a/public/fonts/glyphicons-halflings-regular.svg b/public/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 00000000..94fb5490 --- /dev/null +++ b/public/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,288 @@ + + + \ No newline at end of file diff --git a/public/fonts/glyphicons-halflings-regular.ttf b/public/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 00000000..1413fc60 Binary files /dev/null and b/public/fonts/glyphicons-halflings-regular.ttf differ diff --git a/public/fonts/glyphicons-halflings-regular.woff b/public/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 00000000..9e612858 Binary files /dev/null and b/public/fonts/glyphicons-halflings-regular.woff differ diff --git a/public/fonts/glyphicons-halflings-regular.woff2 b/public/fonts/glyphicons-halflings-regular.woff2 new file mode 100644 index 00000000..64539b54 Binary files /dev/null and b/public/fonts/glyphicons-halflings-regular.woff2 differ diff --git a/public/js/app.js b/public/js/app.js index 88dbd71f..0c6be3dc 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -223,8 +223,13 @@ }) $.getJSON(proxyPath + '/misc/existence', {data: pages}, function (result) { - $.each(result.data, function (href, a) { - $(selector + " a[href='" + proxyPath.split('/').join('\\/') + '\\/wiki\\/' + encodeURIComponent(a) + "']").addClass('absent') + $.each(result.data, function (href, a) { + // MOD change quote convention if page contains ' in its name + if (a.indexOf("'")){ + $(selector + ' a[href="' + proxyPath.split('/').join('\\/') + '\\/wiki\\/' + encodeURIComponent(a) + '"]').addClass('absent') + } else { + $(selector + " a[href='" + proxyPath.split('/').join('\\/') + '\\/wiki\\/' + encodeURIComponent(a) + "']").addClass('absent') + } }) }) } diff --git a/routes/auth.js b/routes/auth.js index 7863f910..57d880cb 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -60,10 +60,10 @@ if (auth.google.enabled) { callbackURL: redirectURL }, - function (accessToken, refreshToken, profile, done) { - usedAuthentication('google') - done(null, profile) - } + function (accessToken, refreshToken, profile, done) { + usedAuthentication('google') + done(null, profile) + } )) } @@ -77,37 +77,37 @@ if (auth.github.enabled) { clientSecret: auth.github.clientSecret, callbackURL: redirectURL }, - function (accessToken, refreshToken, profile, done) { - usedAuthentication('github') - done(null, profile) - } + function (accessToken, refreshToken, profile, done) { + usedAuthentication('github') + done(null, profile) + } )) } if (auth.ldap.enabled) { - passport.use(new passportLDAP(function(req, callback) { - process.nextTick(function() { + passport.use(new passportLDAP(function (req, callback) { + process.nextTick(function () { var bindDn = auth.ldap.bindDn.replace(/{{username}}/g, req.body.username) var bindCredentials = auth.ldap.bindCredentials.replace(/{{password}}/g, req.body.password) var opts = { - server: { - url: auth.ldap.url, - bindDn: bindDn, - bindCredentials: bindCredentials, - searchBase: auth.ldap.searchBase, - searchFilter: auth.ldap.searchFilter, - searchAttributes: auth.ldap.searchAttributes - } + server: { + url: auth.ldap.url, + bindDn: bindDn, + bindCredentials: bindCredentials, + searchBase: auth.ldap.searchBase, + searchFilter: auth.ldap.searchFilter, + searchAttributes: auth.ldap.searchAttributes + } } - callback(null, opts); + callback(null, opts) }) }, - function (profile, done) { - usedAuthentication('ldap') - done(null, profile) - } + function (profile, done) { + usedAuthentication('ldap') + done(null, profile) + } )) } @@ -213,8 +213,8 @@ function _getAuthDone (req, res) { !auth.local.used && !auth.ldap.used && !tools.isAuthorized(res.locals.user.email, - app.locals.config.get('authorization').validMatches, - app.locals.config.get('authorization').emptyEmailMatches)) { + app.locals.config.get('authorization').validMatches, + app.locals.config.get('authorization').emptyEmailMatches)) { req.logout() req.session = null res.statusCode = 403 diff --git a/routes/misc.js b/routes/misc.js index 2dcaa8d3..e8d43067 100644 --- a/routes/misc.js +++ b/routes/misc.js @@ -3,6 +3,7 @@ var router = require('express').Router() var renderer = require('../lib/renderer') var fs = require('fs') var models = require('../lib/models') +var app = require('../lib/app').getInstance() // MOD required to retrieve app configuration models.use(Git) @@ -15,8 +16,11 @@ function _getSyntaxReference (req, res) { } function _postPreview (req, res) { + // MOD redact content prior to rendering + var pageContent = req.body.data + pageContent = renderer.redact(pageContent, res, app.locals.config) res.render('preview', { - content: renderer.render(req.body.data) + content: renderer.render(pageContent) }) } @@ -29,10 +33,27 @@ function _getExistence (req, res) { var result = [] var page var n = req.query.data.length + const aliasMap = app.locals.config.get('aliases') // MOD import aliases + var nameOfPage req.query.data.forEach(function (pageName, idx) { (function (name, index) { - page = new models.Page(name) + // MOD remap alias key to its associated page before retrieving page model + nameOfPage = name + if (aliasMap) { + var aliasName = nameOfPage.toLowerCase() + if (app.locals.config.get('features').caseSensitive) { + aliasName = nameOfPage + } + if (aliasMap[aliasName]) { + nameOfPage = aliasMap[aliasName] + } + } + // MOD capitalize all words in page name + if (!app.locals.config.get('features').caseSensitive){ + nameOfPage = nameOfPage.replace(/(^|\-)\w/g, function(l){ return l.toUpperCase() }) + } + page = new models.Page(nameOfPage) if (!fs.existsSync(page.pathname)) { result.push(name) } diff --git a/routes/pages.js b/routes/pages.js index bf3a2bc3..e6662f4e 100644 --- a/routes/pages.js +++ b/routes/pages.js @@ -166,15 +166,15 @@ function _putPages (req, res) { if (app.locals.config.get('pages').title.fromFilename && page.name.toLowerCase() !== req.body.pageTitle.toLowerCase()) { page.renameTo(req.body.pageTitle) - .then(savePage) - .catch(function (ex) { - errors = [{ - param: 'pageTitle', - msg: 'A page with this name already exists.', - value: '' - }] - fixErrors() - }) + .then(savePage) + .catch(function (ex) { + errors = [{ + param: 'pageTitle', + msg: 'A page with this name already exists.', + value: '' + }] + fixErrors() + }) } else { savePage() } @@ -270,7 +270,6 @@ function _getRevert (req, res) { res.locals.title = '500 – Internal Server Error' res.statusCode = 500 res.render('500.pug') - return } }) } diff --git a/routes/search.js b/routes/search.js index 900d7dd3..9f61fb3c 100644 --- a/routes/search.js +++ b/routes/search.js @@ -4,6 +4,8 @@ var router = require('express').Router() var path = require('path') var corsEnabler = require('../lib/cors-enabler') var models = require('../lib/models') +var renderer = require('../lib/renderer') // MOD import renderer module +var app = require('../lib/app').getInstance() // MOD import app instance for config models.use(Git) @@ -39,18 +41,49 @@ function _getSearch (req, res) { } models.pages.findStringAsync(res.locals.term).then(function (items) { - items.forEach(function (item) { - if (item.trim() !== '') { - record = item.split(':') - res.locals.matches.push({ - pageName: path.basename(record[0].replace(/\.md$/, '')), - line: record[1] ? ', L' + record[1] : '', - text: record.slice(2).join('') - }) - } - }) + // MOD run search results through redaction if enabled (slows response) + if (app.locals.config.get('redaction').enabled) { + var promiseArray = [] + var termRegex - renderResults() + items.forEach(function (item) { + if (item.trim() !== '') { + // MOD retrieve page content and retest for search term after redaction + const record = item.split(':') + const nameOfPage = path.basename(record[0].replace(/\.md$/, '')) + const searchPage = new models.Page(nameOfPage) + promiseArray.push(searchPage.fetch().then(function () { + const redactedContent = renderer.redact(searchPage.content, res, app.locals.config) + termRegex = new RegExp(res.locals.term, 'i') + if (termRegex && termRegex.exec(redactedContent) && redactedContent.includes(record.slice(2).join(''))) { + res.locals.matches.push({ + pageName: nameOfPage, + line: record[1] ? ', L' + record[1] : '', + text: record.slice(2).join('') + }) + } + })) + } + }) + + // MOD wait for all page fetches to return + Promise.all(promiseArray).then(function () { + renderResults() + }) + } else { + items.forEach(function (item) { + if (item.trim() !== '') { + record = item.split(':') + res.locals.matches.push({ + pageName: path.basename(record[0].replace(/\.md$/, '')), + line: record[1] ? ', L' + record[1] : '', + text: record.slice(2).join('') + }) + } + }) + + renderResults() + } }) } diff --git a/routes/wiki.js b/routes/wiki.js index 26485194..339d8415 100644 --- a/routes/wiki.js +++ b/routes/wiki.js @@ -19,13 +19,18 @@ router.get('/wiki/:page/:version', _getWikiPage) router.get('/wiki/:page/compare/:revisions', _getCompare) function _getHistory (req, res) { + + // MOD redact content of sidebar + res.locals._sidebar = renderer.redact(res.locals._sidebar, res, app.locals.config) + var page = new models.Page(req.params.page) page.fetch().then(function () { return page.fetchHistory() }).then(function (history) { // FIXME better manage an error here - if (!page.error) { + // MOD add test to hide history page for anonymous users + if (!page.error && (req.locals.user || !app.locals.config.get('redaction').enabled)) { res.render('history', { items: history, title: 'History of ' + page.name, @@ -40,6 +45,10 @@ function _getHistory (req, res) { } function _getWiki (req, res) { + + // MOD redact content of sidebar + res.locals._sidebar = renderer.redact(res.locals._sidebar, res, app.locals.config) + var items = [] var pagen = 0 | req.query.page @@ -47,10 +56,14 @@ function _getWiki (req, res) { pages.fetch(pagen).then(function () { pages.models.forEach(function (page) { - if (!page.error) { + // MOD test to see if content is redacted + const pageContent = renderer.redact(page.content, res, app.locals.config) + if (!page.error && pageContent) { + const renderedContent = renderer.render(pageContent) // MOD render content items.push({ page: page, - hashes: page.hashes.length === 2 ? page.hashes.join('..') : '' + hashes: page.hashes.length === 2 ? page.hashes.join('..') : '', + summary: renderedContent.match(/[\s\S]*?<\/p>/m) // MOD add first paragraph summary to item }) } }) @@ -69,7 +82,28 @@ function _getWiki (req, res) { } function _getWikiPage (req, res) { - var page = new models.Page(req.params.page, req.params.version) + + // MOD check if page is listed as an alias + var aliasMap = app.locals.config.get('aliases') + var nameOfPage = req.params.page + if (aliasMap) { + var aliasName = nameOfPage.toLowerCase() + if (app.locals.config.get('features').caseSensitive) { + aliasName = nameOfPage + } + if (aliasMap[aliasName]) { + nameOfPage = aliasMap[aliasName] + } + } + // MOD capitalize all words in page name + if (!app.locals.config.get('features').caseSensitive){ + nameOfPage = nameOfPage.replace(/(^|\-)\w/g, function(l){ return l.toUpperCase() }) + } + + // MOD redact content of sidebar + res.locals._sidebar = renderer.redact(res.locals._sidebar, res, app.locals.config) + + var page = new models.Page(nameOfPage, req.params.version) page.fetch().then(function () { if (!page.error) { @@ -82,11 +116,27 @@ function _getWikiPage (req, res) { res.locals.notice = req.session.notice delete req.session.notice - res.render('show', { - page: page, - title: app.locals.config.get('application').title + ' – ' + page.title, - content: renderer.render('# ' + page.title + '\n' + page.content) - }) + // MOD redact content + var pageContent = renderer.redact(page.content, res, app.locals.config) + + // MOD add redirection and render content + if (pageContent) { + var renderedContent = renderer.render('# ' + page.title + '\n' + pageContent) + if (nameOfPage !== req.params.page) { + renderedContent = renderer.redirect(renderedContent, req.params.page, nameOfPage) + } + + res.render('show', { + page: page, + title: app.locals.config.get('application').title + ' – ' + page.title, + content: renderedContent // MOD add rendered content + }) + } else { + // MOD remove existence of page if all content is redacted + res.locals.title = '404 - Not found' + res.statusCode = 404 + res.render('404.pug') + } } else { if (req.user) { // Try sorting out redirect loops with case insentive fs @@ -105,7 +155,6 @@ function _getWikiPage (req, res) { res.locals.title = '404 - Not found' res.statusCode = 404 res.render('404.pug') - return } } } @@ -113,6 +162,10 @@ function _getWikiPage (req, res) { } function _getCompare (req, res) { + + // MOD redact content of sidebar + res.locals._sidebar = renderer.redact(res.locals._sidebar, res, app.locals.config) + var revisions = req.params.revisions var page = new models.Page(req.params.page) @@ -120,7 +173,7 @@ function _getCompare (req, res) { page.fetch().then(function () { return page.fetchRevisionsDiff(revisions) }).then(function (diff) { - if (!page.error) { + if (!page.error && (req.locals.user || !app.locals.config.get('redaction').enabled)) { var lines = [] diff.split('\n').slice(4).forEach(function (line) { if (line.slice(0, 1) !== '\\') { @@ -144,7 +197,6 @@ function _getCompare (req, res) { res.locals.title = '404 - Not found' res.statusCode = 404 res.render('404.pug') - return } }) diff --git a/test/spec/configSpec.js b/test/spec/configSpec.js index 2379c477..841a21d9 100644 --- a/test/spec/configSpec.js +++ b/test/spec/configSpec.js @@ -3,7 +3,7 @@ var yaml = require('js-yaml') -var configKeys = ['application', 'authentication', 'features', 'server', 'authorization', 'pages', 'customizations'] +var configKeys = ['application', 'authentication', 'features', 'server', 'authorization', 'pages', 'customizations', 'assets', 'aliases', 'layout', 'redaction'] var Config = require('../../lib/config') describe('Config', function () { @@ -22,6 +22,8 @@ describe('Config', function () { expect(def.application.title).to.equal('Jingo') expect(def.application.repository).to.equal('') expect(def.application.docSubdir).to.equal('') + expect(def.application.mediaSubdir).to.equal('') + expect(def.application.serveLocal).to.be.false expect(def.application.remote).to.equal('') expect(def.application.pushInterval).to.equal(30) expect(def.application.secret).to.equal('change me') @@ -30,11 +32,14 @@ describe('Config', function () { expect(def.application.loggingMode).to.equal(1) expect(def.application.pedanticMarkdown).to.be.true expect(def.application.gfmBreaks).to.be.true + expect(def.application.percolateBlocks).to.equal('div, blockquote') expect(def.application.staticWhitelist).to.equal('/\\.png$/i, /\\.jpg$/i, /\\.gif$/i') expect(def.application.proxyPath).to.equal('') expect(def.features.codemirror).to.be.true expect(def.features.markitup).to.be.false + expect(def.features.pageSummaries).to.be.true + expect(def.features.caseSensitive).to.be.false expect(def.server.hostname).to.equal('localhost') expect(def.server.port).to.equal(6067) @@ -48,6 +53,19 @@ describe('Config', function () { expect(def.authentication.google.enabled).to.be.true expect(def.authentication.local.enabled).to.be.false expect(def.authentication.github.enabled).to.be.false + + expect(def.assets.css).to.equal('') + expect(def.assets.js).to.equal('') + + expect(def.layout.sidebarWidth).to.equal(2) + expect(def.layout.mainWidth).to.equal(8) + expect(def.layout.footerWidth).to.equal(8) + expect(def.layout.sidebarMobile).to.be.true + + expect(def.redaction.enabled).to.be.false + expect(def.redaction.hiddenPage).to.equal('^') + expect(def.redaction.privateContent).to.equal('') + expect(def.redaction.futureContent).to.equal('([\\s\\S]*?)') }) it('should get the config as a whole', function () { diff --git a/test/spec/redactorSpec.js b/test/spec/redactorSpec.js new file mode 100644 index 00000000..2ead016a --- /dev/null +++ b/test/spec/redactorSpec.js @@ -0,0 +1,83 @@ +/* eslint-env mocha */ +/* global expect */ + +var Redactor = require('../../lib/redactor') + +var config = { + get: function(key){ + return this[key] + }, + redaction: { + enabled: true, + hiddenPage: '^', + privateContent: '', + futureContent: '([\\s\\S]*?)', + sectionContent: [{ + expression: '([\\s\\S]*?)', + current: 0 + }] + } +} + +var authRes = { + get: function(key){ + return this[key] + }, + locals: { user: true } +} +var anonRes = { + get: function(key){ + return this[key] + }, + locals: { user: false } +} + + +describe('Redactor', function () { + it('should add redirection paragraph', function () { + var html = '
This is some text
' + var requested_page = 'foo' + var redirected_page = 'Bar.md' + expect(Redactor.redirect(html, requested_page, redirected_page)).to.be.equal('"foo" redirects here.
This is some text
') + }) + + it('should redact all hidden content from anonymous', function () { + var text = 'Some hidden foo.' + expect(Redactor.redact(text, anonRes, config)).to.be.equal('') + }) + + it('should reformat hidden content for authenticated', function () { + var text = 'Some hidden foo.' + expect(Redactor.redact(text, authRes, config)).to.be.equal('-- Hidden --Some hidden foo.') + }) + + it('should redact private content from anonymous', function () { + var text = 'Some visible foo' + expect(Redactor.redact(text, anonRes, config)).to.be.equal('Some visible foo') + }) + + it('should reformat private content for authenticated', function () { + var text = 'Some visible foo' + expect(Redactor.redact(text, authRes, config)).to.be.equal('Some visible foo Some hidden bar') + }) + + it('should redact future content from anonymous', function () { + var text = 'Some visible fooSome hidden bar' + expect(Redactor.redact(text, anonRes, config)).to.be.equal('Some visible foo') + }) + + it('should reformat future content for authenticated', function () { + var text = 'Some visible fooSome hidden bar' + expect(Redactor.redact(text, authRes, config)).to.be.equal('Some visible foo-- 2099.09.09 --Some hidden bar-- End --') + }) + + it('should redact section content from anonymous', function () { + var text = 'Some visible fooSome hidden bar' + expect(Redactor.redact(text, anonRes, config)).to.be.equal('Some visible foo') + }) + + it('should reformat section content for authenticated', function () { + var text = 'Some visible fooSome hidden bar' + expect(Redactor.redact(text, authRes, config)).to.be.equal('Some visible foo-- chapter-1 --Some hidden bar-- End --') + }) +}) \ No newline at end of file diff --git a/views/layout.pug b/views/layout.pug index 05ca133c..00ec4740 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -11,10 +11,18 @@ include mixins/links meta(name="viewport", content="width=device-width, initial-scale=1") title= title +asset("/vendor/bootstrap/css/bootstrap.min.css") - link(rel="stylesheet", href="https://fonts.googleapis.com/css?family=Ubuntu|Ubuntu+Condensed") + //- MOD serve fonts from local files + if serveLocal + +asset("/css/google-fonts.css") + else + link(rel="stylesheet", href="https://fonts.googleapis.com/css?family=Ubuntu|Ubuntu+Condensed") +asset("/css/style.css") +asset("/css/ionicons.min.css") +asset("/css/shCoreDefault.css") + //- MOD add css assets + if cssAssets.length + each sheet in cssAssets + +asset(sheet) block styles if hasCustomStyle() style. @@ -48,25 +56,45 @@ include mixins/links .container + //- MOD customization of column width + -var sidebar_class = 'col-md-' + sidebarWidth + ' with-sidebar' + -var sidebar_mobile_class = sidebar_class + ' hidden-sm hidden-xs' + -var main_class = 'col-md-' + mainWidth + ' hide-tools' + -var footer_sidebar = 'col-md-' + footerSidebarWidth + -var footer_class = 'col-md-' + footerWidth + ' with-footer' + -var sidebar_class_none = 'col-md-' + sidebarWidth + -var sidebar_mobile_class_none = sidebar_class_none + ' hidden-sm hidden-xs' + .row if hasSidebar() - .col-md-2.with-sidebar - .content !{_sidebar} + if sidebarMobile + div(id='sidebar' class=sidebar_class) + .content !{_sidebar} + else + div(id='sidebar' class=sidebar_mobile_class) + .content !{_sidebar} else - .col-md-2 + if sidebarMobile + div(id='sidebar' class=sidebar_class_none) + else + div(id='sidebar' class=sidebar_mobile_class_none) - #main.hide-tools.col-md-8 + div(id='main' class=main_class) block content if hasFooter() .row - .col-md-2 - .col-md-8.with-footer + div(class=footer_sidebar) + div(id='footer' class=footer_class) .content !{_footer} script(src=proxyPath + "/vendor/jquery.min.js") +asset("/vendor/bootstrap/js/bootstrap.min.js") +asset("/js/app.js") + //- MOD add javascript assets + if jsAssets.length + each js in jsAssets + +asset(js) script. Jingo.init("#{proxyPath}"); block scripts diff --git a/views/list.pug b/views/list.pug index e3e0d95e..3eed0efc 100644 --- a/views/list.pug +++ b/views/list.pug @@ -10,26 +10,59 @@ block content a(href=item.page.urlForShow()) #{item.page.title} div.content.clearfix .meta - |Last update by - if hasGravatar() - img(src=gravatar().url(item.page.metadata.email, {s:16})) - b #{item.page.metadata.author}, - b.date(title=item.page.metadata.date) #{item.page.metadata.relDate} - | – #{item.page.metadata.hash} - ul.page-actions - if !isAnonymous() - li - a(href=item.page.urlForEdit(), title="Edit this page").btn.btn-default - i.icon.ion-compose - li - a(href=item.page.urlForHistory(), title="Page history").btn.btn-default - i.icon.ion-clock - if item.hashes - li - a(href=`${item.page.urlForCompare()}/${item.hashes}`, title="Quick diff").btn.btn-default - i.icon.ion-shuffle - .message - |→ #{item.page.lastCommitMessage} + //- MOD reformat commit history if redact is enabled + if redactEnabled + |Last updated + b.date(title="#{item.page.metadata.date}") #{item.page.metadata.relDate} + if !isAnonymous() + | by + b #{item.page.metadata.author} + | – #{item.page.metadata.hash} + else + |Last update by + if hasGravatar() + img(src=gravatar().url(item.page.metadata.email, {s:16})) + b #{item.page.metadata.author}, + b.date(title=item.page.metadata.date) #{item.page.metadata.relDate} + | – #{item.page.metadata.hash} + + if redactEnabled + if !isAnonymous() + ul.page-actions + li + a(href=item.page.urlForEdit(), title="Edit this page").btn.btn-default + i.icon.ion-compose + li + a(href=item.page.urlForHistory(), title="Page history").btn.btn-default + i.icon.ion-clock + if item.hashes + li + a(href=`${item.page.urlForCompare()}/${item.hashes}`, title="Quick diff").btn.btn-default + i.icon.ion-shuffle + if pageSummaries + .message + | #{item.summary} + .message.commit-font + |→ #{item.page.lastCommitMessage} + else + if pageSummaries + .message + | #{item.summary} + else + ul.page-actions + if !isAnonymous() + li + a(href=item.page.urlForEdit(), title="Edit this page").btn.btn-default + i.icon.ion-compose + li + a(href=item.page.urlForHistory(), title="Page history").btn.btn-default + i.icon.ion-clock + if item.hashes + li + a(href=`${item.page.urlForCompare()}/${item.hashes}`, title="Quick diff").btn.btn-default + i.icon.ion-shuffle + .message + |→ #{item.page.lastCommitMessage} ul.paginator each pageNumber in pageNumbers diff --git a/views/mixins/form.pug b/views/mixins/form.pug index afe79425..546c6887 100644 --- a/views/mixins/form.pug +++ b/views/mixins/form.pug @@ -19,10 +19,11 @@ mixin tools(action, pageName) li +anchor("/pages/" + pageName + "/edit")(title="Edit this page").btn.btn-sm.btn-default i.icon.ion-compose - - li - +anchor("/wiki/" + pageName + "/history")(title="Page history").btn.btn-sm.btn-default - i.icon.ion-clock + //- MOD add history option only if content is not redacted + if !redactContent + li + +anchor("/wiki/" + pageName + "/history")(title="Page history").btn.btn-sm.btn-default + i.icon.ion-clock li +anchor("/wiki")(title="All pages").btn.btn-sm.btn-default i.icon.ion-grid diff --git a/views/show.pug b/views/show.pug index 0a59fa65..56268f88 100644 --- a/views/show.pug +++ b/views/show.pug @@ -16,10 +16,11 @@ block content .jingo-content.jingo-show !=content - var klass = isAjax ? 'jingo-footer' : 'footer' - p(class=klass) Updated by - if hasGravatar() - img(src=gravatar().url(page.metadata.email, {s:16})) - b #{page.metadata.author} - |, - b(title=page.metadata.date) #{page.metadata.relDate} - | – #{page.metadata.hash} + if !redactContent + p(class=klass) Updated by + if hasGravatar() + img(src=gravatar().url(page.metadata.email, {s:16})) + b #{page.metadata.author} + |, + b(title=page.metadata.date) #{page.metadata.relDate} + | – #{page.metadata.hash}