Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
491332a378 | |||
6c8d403712 |
11
.drone.yml
11
.drone.yml
|
@ -1,11 +0,0 @@
|
|||
kind: pipeline
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: crystallang/crystal:latest
|
||||
environment:
|
||||
commands:
|
||||
- make deps
|
||||
- make test
|
||||
- make build
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,12 +1,7 @@
|
|||
/doc/
|
||||
/docs/
|
||||
/lib/
|
||||
/bin/
|
||||
/.shards/
|
||||
/.crystal/
|
||||
.env
|
||||
|
||||
/wikicr
|
||||
/data/
|
||||
/meta/
|
||||
|
||||
|
|
14
Makefile
14
Makefile
|
@ -3,24 +3,22 @@ NAME=wikicr
|
|||
all: deps_opt build
|
||||
|
||||
run:
|
||||
crystal run src/$(NAME).cr --error-trace
|
||||
crystal run src/$(NAME).cr
|
||||
build:
|
||||
crystal build src/$(NAME).cr --stats --error-trace
|
||||
crystal build src/$(NAME).cr --stats
|
||||
release:
|
||||
crystal build src/$(NAME).cr --stats --release
|
||||
test:
|
||||
crystal spec --error-trace
|
||||
crystal spec
|
||||
deps:
|
||||
shards install
|
||||
crystal deps install
|
||||
deps_update:
|
||||
shards update
|
||||
crystal deps update
|
||||
deps_opt:
|
||||
@[ -d lib/ ] || make deps
|
||||
doc:
|
||||
crystal docs
|
||||
clean:
|
||||
rm $(NAME)
|
||||
format:
|
||||
crystal tool format
|
||||
|
||||
.PHONY: all run build release test deps deps_update clean doc format
|
||||
.PHONY: all run build release test deps deps_update clean doc
|
||||
|
|
186
README.md
186
README.md
|
@ -1,169 +1,79 @@
|
|||
**Migrated to <https://git.sceptique.eu/Sceptique/wikicr>**
|
||||
|
||||
# wikicr
|
||||
|
||||
[![Build Status](https://drone.sceptique.eu/api/badges/Sceptique/wikicr/status.svg)](https://drone.sceptique.eu/Sceptique/wikicr)
|
||||
|
||||
Wiki in crystal and markdown
|
||||
|
||||
The pages of the wiki are written in markdown and committed on the git repository where it is started.
|
||||
The pages of the wiki are written in markdown and commited on the git repository where it is started.
|
||||
|
||||
## How to install
|
||||
|
||||
### Dependencies
|
||||
|
||||
Verify that you have crystal v1.0.0 or greater installed, as well as shards and git.
|
||||
|
||||
### Get the application
|
||||
|
||||
git clone https://github.com/Nephos/wikicr.git
|
||||
cd wikicr
|
||||
|
||||
### Test the application
|
||||
|
||||
make test
|
||||
|
||||
### Build the binary
|
||||
## Installation
|
||||
|
||||
make
|
||||
|
||||
### Run the server
|
||||
## Usage
|
||||
|
||||
./wikicr --port 3000
|
||||
INVERT_THEME=true ./wikicr --port 3000 # dark mode
|
||||
wikicr --help
|
||||
-b HOST, --bind HOST Host to bind (defaults to 0.0.0.0)
|
||||
-p PORT, --port PORT Port to listen for connections (defaults to 3000)
|
||||
-s, --ssl Enables SSL
|
||||
--ssl-key-file FILE SSL key file
|
||||
--ssl-cert-file FILE SSL certificate file
|
||||
-h, --help Shows this help
|
||||
|
||||
### Verify your files
|
||||
### Configuration
|
||||
|
||||
A directory `meta/` should be created into wikicr.
|
||||
It must contains several files and directories (acl, index, users, ...).
|
||||
You may want to save this directory because it contains meta-data about the pages.
|
||||
#### Environment variables
|
||||
|
||||
Another `data/` should be a git repository (initialized at the first start).
|
||||
Those files are the ALL the "displayed data" of the wiki.
|
||||
- WIKI_DATA (default "data/"): set the directory where the data will be stored. It will be removed in the future for a configuation manager
|
||||
- WIKI_SECRET: random value that init the sessions (`crystal eval 'require "secure_random"; puts SecureRandom.hex(64)'`)
|
||||
|
||||
## Security and ACLs
|
||||
|
||||
* Admin panel to manage the directories and pages
|
||||
* Rules on directories are terminated with a \*
|
||||
* If several rules conflict, take the more specific one
|
||||
* Directories with the longer name prevails
|
||||
* Files rules prevails over directory rules
|
||||
|
||||
## Administration and usage tutorial
|
||||
|
||||
### Edit / Create a page
|
||||
<img width=240 src="https://i.imgur.com/5bfJstb.png" />
|
||||
|
||||
### Show a page
|
||||
<img width=240 src="https://i.imgur.com/gllJ8Nr.png" />
|
||||
|
||||
### Remove a page
|
||||
Simply edit the page to remove and delete all the content.
|
||||
The page will be deleted completely.
|
||||
|
||||
### Administrate users and acls
|
||||
<img width=240 src="https://i.imgur.com/1zWiAV3.png" />
|
||||
|
||||
### Custom Markdown
|
||||
A special markdown (wikimd) is used in the pages. It provides several interesting features:
|
||||
|
||||
#### Internal links
|
||||
An internal link will search through the index of pages to find a matching one and render a valid link to it.
|
||||
|
||||
```markdown
|
||||
blabla [[my page]] blabla
|
||||
```
|
||||
|
||||
##### Notes about the wikimd
|
||||
- internal link algorithm have been benchmarked a bit
|
||||
[benchmark link](https://gist.github.com/Nephos/ad292a3e2acc9201e6ea6342eb85dacb)
|
||||
The algorithm has been improved since, but it gave me a first idea of what to do.
|
||||
|
||||
## Development and Roadmap
|
||||
|
||||
### You want to add or modify something ?
|
||||
|
||||
Don't hesitate to open an issue, I'm always happy to discuss about my projects
|
||||
or include other developers than me.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Open an issue to see what you want to implement and how to do it.
|
||||
2. Fork it ( https://github.com/Nephos/wikicr/fork )
|
||||
3. Create your feature branch (git checkout -b my-new-feature)
|
||||
4. Commit your changes (git commit -am 'Add some feature')
|
||||
5. Push to the branch (git push origin my-new-feature)
|
||||
6. Create a new Pull Request
|
||||
## Development
|
||||
|
||||
### Operations
|
||||
|
||||
For now, there is no "important" core operation to add (they are all already implemented).
|
||||
However, there are still lots of improvements to write on the current implementation,
|
||||
documentation, security check, error management and consistency of the code.
|
||||
|
||||
- [x] (core) View wiki pages
|
||||
- [x] (core) Write new wiki page, edit existing ones
|
||||
- [x] (core) Chroot the files to data/: avoid writing / reading files outside of data/
|
||||
- [x] (core) If page does not exists, form to create it: if a file does not exist, display the edit form
|
||||
- [x] (core) Delete pages: remove the content of a page should remove the file
|
||||
- [x] (core) Index of pages: each modification of a page should update and index with all these pages (with first h1 and url)
|
||||
- [x] (core) Choose between sqlite3 and the file system for the index: sqlite = sql, fs = easier
|
||||
- [x] (core) Move page (rename): box with "mv X Y" and git commit
|
||||
- [x] (core) Lock system for acl/users/index: A thread safe method should be provided to avoid conflict when executing read+write or write operations
|
||||
* [x] (core) View wiki pages
|
||||
* [x] (core) Write new wiki page, edit existing ones
|
||||
* [x] (core) Chroot the files to data/: avoid writing / reading files outside of data/
|
||||
* [x] (core) If page does not exists, form to create it: if a file does not exist, display the edit form
|
||||
* [x] (core) Delete pages: remove the content of a page should remove the file
|
||||
* [ ] (core) Index of pages: each modification of a page should update and index with all thes pages (with first h1 and url)
|
||||
* [ ] (core) Choose between sqlite3 and the filesystem for the index: sqlite = sql, fs = easier
|
||||
* [ ] (core) Move page (rename): box with "mv X Y" and git commit
|
||||
|
||||
### Git
|
||||
|
||||
At the beginning, I tried to used libgit2. However, it seems to be a bad idea
|
||||
because the lib was not documented (no tutorial or at least not up-to-date, API
|
||||
not very well documented, etc.) so I want to write a little git-* wrapper to
|
||||
handle some operations (add, commit, revert, etc.).
|
||||
|
||||
It is not something very likely to be done first (even if it's a lot of important
|
||||
features) because it is boring and requires to take care of the security issues.
|
||||
I must have to replace the "system" calls (in backquote) with `Proccess.new.run`.
|
||||
|
||||
- [x] (git) Commit when write on a file: every modification on data/ should be committed
|
||||
- [ ] (git) List of revisions on a file (using git): list the revision of a file
|
||||
- [ ] (git) Revert a revision (avoid vandalism): button to remove a revision (git revert)
|
||||
* [ ] (git) Commit when write on a file: every modification on data/ should be commited
|
||||
* [ ] (git) List of revisions on a file (using git): list the revision of a file
|
||||
* [ ] (git) Revert a revision (avoid vandalism): button to remove a revision (git revert)
|
||||
|
||||
### Web
|
||||
|
||||
There is some important features in order to have a good interface and a fluent
|
||||
wiki experience. That's not the stuff I prefer because it requires some css/js
|
||||
(front-end stuff).
|
||||
|
||||
There is also work around string matching to write a valid research engine.
|
||||
This is the most important feature to add right now.
|
||||
|
||||
- [x] (web) Add content table: if titles are written, give a content table with them and links to anchors
|
||||
- [x] (web) Sitemap: add a list of all the files available
|
||||
- [x] (web) User login / registration: keep a file with login:group:bcryptpassords
|
||||
- [x] (web) User ACL basic (read / write): the groups have rights on directories (globing)
|
||||
- [x] (web) Groups ACL on EVERY wiki url
|
||||
- [ ] (web) Search a page: an input that search a page (content, title) with auto-completion
|
||||
- [ ] (web) Template loader (files in public/): load css, js etc. from public/
|
||||
- [ ] (web) File upload and lists: page that add a file in uploads/
|
||||
- [ ] (web) Tags for pages (index): extended markdown and index to keep a list of pages
|
||||
* [ ] (web) Add content table: if titles are written, give a content table with them and links to anchors
|
||||
* [x] (web) Sitemap: add a list of all the files available
|
||||
* [ ] (web) Search a page: an input that search a page (content, title) with autocompletion
|
||||
* [x] (web) User login / registration: keep a file with login:group:bcryptpassords
|
||||
* [ ] (web) User LDAP basic (read / write): the groups have rights on directories
|
||||
* [ ] (web) Tags for pages (index): extended markdown and index to keep a list of pages
|
||||
* [ ] (web) Template loader (files in public/): load css, js etc. from public/
|
||||
* [ ] (web) File upload and lists: page that add a file in uploads/
|
||||
|
||||
### Advanced usage
|
||||
|
||||
The current implementation of Markdown in crystal is limited and may be fully rewritten with more standard features in some weeks or months.
|
||||
For now, I choose to use Markd, another markdown parser, and wrote a wikimd wrapper (Wikicr::Page::Markdown).
|
||||
It allows me to expand the default markdown by writting HTML inside the markdown to render.
|
||||
|
||||
The rest is boring stuff (code factorization, make everything configurable, document the code, add a lot of specs, ...).
|
||||
|
||||
- [x] (edit) Handle `[[tag]]`: markdown extended to search in the page index (url and title)
|
||||
- [x] (edit) Handle `[[tag|title]]`: same than internal links but with a fixed title
|
||||
- [ ] (core) Index the internal links of a page to update them if a page is move or the title changed.
|
||||
- [ ] (web) Configuration page: title of the wiki, rights of the files, etc. should be configurable
|
||||
- [ ] (conf) Handle environment variables in a .env file
|
||||
- [ ] (core) Extensions loader (.so files + extended markdown ?): extend the wiki features with hooks
|
||||
* [ ] (core) Extensions loader (.so files + extended markdown ?): extend the wiki features with hooks
|
||||
* [ ] (web) Configuration page: title of the wiki, rights of the files, etc. should be configurable
|
||||
* [ ] (edit) Handle `[[tag]]`: markdown extended to search in the page index (url and title)
|
||||
* [ ] (conf) Handle environemnt variables in a .env file
|
||||
|
||||
### Other
|
||||
|
||||
- [x] Improve the controller/routes architecture
|
||||
* [ ] take a look at https://github.com/kemalyst/kemalyst
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork it ( https://github.com/Nephos/wikicr/fork )
|
||||
2. Create your feature branch (git checkout -b my-new-feature)
|
||||
3. Commit your changes (git commit -am 'Add some feature')
|
||||
4. Push to the branch (git push origin my-new-feature)
|
||||
5. Create a new Pull Request
|
||||
|
||||
## Contributors
|
||||
|
||||
- [Sceptique](https://git.sceptique.eu/Sceptique) Arthur Poulet - creator, maintainer
|
||||
- [Nephos](https://github.com/Nephos) Arthur Poulet - creator, maintainer
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
require "./routes"
|
||||
|
||||
Kemal::Session.config do |config|
|
||||
config.cookie_name = "session_id"
|
||||
config.secret = ENV["WIKI_SECRET"]? || Random::Secure.base64(64)
|
||||
config.gc_interval = 2.minutes # 2 minutes
|
||||
end
|
||||
|
||||
Kemal.run
|
|
@ -1,7 +0,0 @@
|
|||
FROM crystallang/crystal:lastest
|
||||
|
||||
WORKDIR /app/user
|
||||
|
||||
ADD . /app/user
|
||||
|
||||
RUN crystal deps
|
|
@ -1,39 +0,0 @@
|
|||
class Router
|
||||
{% for verb in {:get, :post, :delete, :patch, :put, :head} %}
|
||||
macro {{verb.id}}(route, controller, method)
|
||||
::{{verb.id}}(\{{route}}) do |env|
|
||||
context = \{{controller}}.new(env)
|
||||
# puts "Before init"
|
||||
# pp env.request.cookies
|
||||
# pp env.response.cookies
|
||||
context.cookies.fill_from_client_headers(env.request.headers)
|
||||
# puts "After init"
|
||||
# pp env.request.cookies
|
||||
# pp env.response.cookies
|
||||
output = context.\{{method.id}}()
|
||||
# puts "After controller"
|
||||
# pp env.request.cookies
|
||||
# pp env.response.cookies
|
||||
#context.cookies.add_response_headers(env.response.headers)
|
||||
output
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
|
||||
Router.get "/", HomeController, :index
|
||||
Router.get "/sitemap", PagesController, :sitemap
|
||||
Router.get "/pages/search", PagesController, :search
|
||||
Router.get "/pages/*path", PagesController, :show
|
||||
Router.post "/pages/*path", PagesController, :update
|
||||
Router.get "/users/login", UsersController, :login
|
||||
Router.post "/users/login", UsersController, :login_validates
|
||||
Router.get "/users/register", UsersController, :register
|
||||
Router.post "/users/register", UsersController, :register_validates
|
||||
Router.get "/admin/users", AdminController, :users_show
|
||||
Router.post "/admin/users/create", AdminController, :user_create
|
||||
Router.post "/admin/users/delete", AdminController, :user_delete
|
||||
Router.get "/admin/acls", AdminController, :acls_show
|
||||
Router.post "/admin/acls/update", AdminController, :acl_update
|
||||
Router.post "/admin/acls/create", AdminController, :acl_create
|
||||
Router.post "/admin/acls/delete", AdminController, :acl_delete
|
|
@ -1,17 +0,0 @@
|
|||
version: '2'
|
||||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
image: wikicr
|
||||
command: './wikicr'
|
||||
working_dir: /app/user
|
||||
environment:
|
||||
ports:
|
||||
- '3000:3000'
|
||||
depends_on:
|
||||
volumes:
|
||||
- '.:/app/user'
|
||||
|
||||
volumes:
|
||||
db:
|
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,68 +1,13 @@
|
|||
form#edit-page textarea {
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
border: 4px solid #557766;
|
||||
form#edit-page {
|
||||
textarea {
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
border: 4px solid #556677;
|
||||
|
||||
padding: 10px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
padding: 0px 0px 0px 0px;
|
||||
margin: 0px 0px 0px 1.5em;
|
||||
}
|
||||
|
||||
#page-body {
|
||||
box-shadow: 0 0 0.5em #999;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
#page-body-meta {
|
||||
box-shadow: -2px 2px 2px #ccc;
|
||||
padding: 0 0.5em 0.5em 1.5em;
|
||||
margin: 0 0 0.5em 1.0em;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.page-body-meta-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#page-table-of-content ul, #page-table-of-content ol {
|
||||
padding: 0px 0px 0px 0px;
|
||||
margin: 0px 0px 0px 0.5em;
|
||||
}
|
||||
|
||||
#page-table-of-content ol {
|
||||
counter-reset: item;
|
||||
}
|
||||
|
||||
#page-table-of-content li {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#page-table-of-content li:before {
|
||||
content: counters(item, ".") ".";
|
||||
counter-increment: item;
|
||||
padding-right:10px;
|
||||
margin-left:-20px;
|
||||
}
|
||||
|
||||
h1:hover > a.anchor::before,
|
||||
h2:hover > a.anchor::before,
|
||||
h3:hover > a.anchor::before,
|
||||
h4:hover > a.anchor::before,
|
||||
h5:hover > a.anchor::before,
|
||||
h6:hover > a.anchor::before {
|
||||
font-family: "FontAwesome";
|
||||
content: "\f0c1";
|
||||
margin: -20px; /* this put the link in the padding of the content <insert circus music here> <laugh> */
|
||||
}
|
||||
|
||||
a.anchor {
|
||||
text-decoration: none;
|
||||
font-size: 14pt;
|
||||
padding: 10px;
|
||||
margin: 50px auto;
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,41 +0,0 @@
|
|||
@supports (backdrop-filter: invert(1)) {
|
||||
#mybpwaycfxccmnp-dblt-backdrop-filter {
|
||||
display: block !important;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
margin: 0 !important;
|
||||
pointer-events: none !important;
|
||||
z-index: 2147483647 !important;
|
||||
backdrop-filter: invert(1) hue-rotate(180deg) !important;
|
||||
}
|
||||
img:not(.mwe-math-fallback-image-inline):not([alt="inline_formula"]),
|
||||
video {
|
||||
filter: invert(1) hue-rotate(180deg) !important;
|
||||
}
|
||||
}
|
||||
@supports not (backdrop-filter: invert(1)) {
|
||||
html,
|
||||
img:not(.mwe-math-fallback-image-inline):not([alt="inline_formula"]),
|
||||
video,
|
||||
div#viewer.pdfViewer div.page {
|
||||
filter: invert(1) hue-rotate(180deg) !important;
|
||||
}
|
||||
:fullscreen video,
|
||||
video:fullscreen {
|
||||
filter: none !important;
|
||||
}
|
||||
html {
|
||||
background-color: black !important;
|
||||
}
|
||||
}
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
|
||||
<cross-domain-policy>
|
||||
<!-- Read this: www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html -->
|
||||
|
||||
<!-- Most restrictive policy: -->
|
||||
<site-control permitted-cross-domain-policies="none"/>
|
||||
|
||||
<!-- Least restrictive policy: -->
|
||||
<!--
|
||||
<site-control permitted-cross-domain-policies="all"/>
|
||||
<allow-access-from domain="*" to-ports="*" secure="false"/>
|
||||
<allow-http-request-headers-from domain="*" headers="*" secure="false"/>
|
||||
-->
|
||||
</cross-domain-policy>
|
|
@ -1,3 +0,0 @@
|
|||
# http://www.robotstxt.org
|
||||
User-agent: *
|
||||
Disallow:
|
44
shard.lock
44
shard.lock
|
@ -1,38 +1,30 @@
|
|||
version: 2.0
|
||||
version: 1.0
|
||||
shards:
|
||||
backtracer:
|
||||
git: https://github.com/sija/backtracer.cr.git
|
||||
version: 1.2.1
|
||||
|
||||
exception_page:
|
||||
git: https://github.com/crystal-loot/exception_page.git
|
||||
version: 0.2.0
|
||||
git:
|
||||
github: mosop/git
|
||||
commit: 27cc31b40fc358457b37ab12b2b572c97200755b
|
||||
|
||||
kemal:
|
||||
git: https://github.com/kemalcr/kemal.git
|
||||
version: 1.1.0+git.commit.5e2efec0503622267b57d03fe7d5151000d3bf47
|
||||
|
||||
kemal-flash:
|
||||
git: https://github.com/nephos/kemal-flash.git
|
||||
version: 0.1.0+git.commit.d1ea6ce9caaed840a062e07a9223cc404cf3adc7
|
||||
github: kemalcr/kemal
|
||||
commit: eb3e83d5903ce8f18d796a97ae200009fdf2c9a2
|
||||
|
||||
kemal-session:
|
||||
git: https://github.com/kemalcr/kemal-session.git
|
||||
version: 1.0.0+git.commit.2ebaf68ed48da763a9cdd0f8dcbc6347981e115d
|
||||
github: kemalcr/kemal-session
|
||||
commit: bbef25f36fe6505d3221de9952f94a1023225b85
|
||||
|
||||
kilt:
|
||||
git: https://github.com/jeromegn/kilt.git
|
||||
version: 0.6.1
|
||||
|
||||
markd:
|
||||
git: https://github.com/nephos/markd.git
|
||||
version: 0.4.1+git.commit.8b67caf4e6ecd89d9dc533187acacf7bc835d05f
|
||||
github: jeromegn/kilt
|
||||
version: 0.4.0
|
||||
|
||||
radix:
|
||||
git: https://github.com/luislavena/radix.git
|
||||
version: 0.4.1
|
||||
github: luislavena/radix
|
||||
version: 0.3.8
|
||||
|
||||
safec:
|
||||
github: mosop/safec
|
||||
version: 0.2.1
|
||||
|
||||
slang:
|
||||
git: https://github.com/jeromegn/slang.git
|
||||
version: 1.7.3+git.commit.901df64c9a1c1df91d3acd00762509c9b6d3cfd8
|
||||
github: jeromegn/slang
|
||||
commit: b817c89c7e5ae39562710c0d6c7f42cee685e14f
|
||||
|
||||
|
|
29
shard.yml
29
shard.yml
|
@ -1,28 +1,27 @@
|
|||
name: wikicr
|
||||
version: 0.2.0
|
||||
version: 0.1.0
|
||||
|
||||
authors:
|
||||
- Arthur Poulet <arthur.poulet@sceptique.eu>
|
||||
- Arthur Poulet <arthur.poulet@mailoo.org>
|
||||
|
||||
crystal: ~> 1.0.0
|
||||
targets:
|
||||
wikicr:
|
||||
main: src/wikicr.cr
|
||||
|
||||
license: MIT
|
||||
crystal: 0.22.0
|
||||
|
||||
license: GPL-3.0
|
||||
|
||||
dependencies:
|
||||
kemal:
|
||||
github: kemalcr/kemal
|
||||
branch: master
|
||||
kilt:
|
||||
github: jeromegn/kilt
|
||||
slang:
|
||||
github: jeromegn/slang
|
||||
branch: master
|
||||
kemal-session:
|
||||
github: kemalcr/kemal-session
|
||||
branch: master
|
||||
kemal-flash:
|
||||
github: Nephos/kemal-flash
|
||||
branch: bugfix/update-crystal-json
|
||||
markd:
|
||||
github: Nephos/markd
|
||||
branch: feature-toc-fix
|
||||
slang:
|
||||
github: jeromegn/slang
|
||||
branch: master
|
||||
git:
|
||||
github: mosop/git
|
||||
branch: master
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
describe Acl do
|
||||
it "test the users permissions" do
|
||||
acls = Acl::Groups.new File.tempfile("spec").to_s
|
||||
g1 = Acl::Group.new(
|
||||
name: "user",
|
||||
default: Acl::Perm::Read,
|
||||
permissions: {
|
||||
"/tmp/protected" => Acl::Perm::None,
|
||||
"/tmp/write/*" => Acl::Perm::Write,
|
||||
"/match/*" => Acl::Perm::Write,
|
||||
"/match/not-file" => Acl::Perm::None,
|
||||
"/match/not-dir/*" => Acl::Perm::None,
|
||||
})
|
||||
g2 = Acl::Group.new(
|
||||
name: "admin",
|
||||
permissions: {
|
||||
"/match/*" => Acl::Perm::Read,
|
||||
},
|
||||
default: Acl::Perm::Write)
|
||||
acls.add g1
|
||||
acls.add g2
|
||||
u1 = Wikicr::User.new "u1", "", %w(user)
|
||||
u2 = Wikicr::User.new "u2", "", %w(user admin)
|
||||
|
||||
# simple
|
||||
acls.permitted?(u1, "/", Acl::Perm::Read).should be_true
|
||||
acls.permitted?(u1, "/tmp", Acl::Perm::Read).should be_true
|
||||
acls.permitted?(u1, "/tmp", Acl::Perm::Write).should be_false
|
||||
acls.permitted?(u1, "/tmp/protected", Acl::Perm::Read).should be_false
|
||||
acls.permitted?(u2, "/tmp/protected", Acl::Perm::Read).should be_true
|
||||
|
||||
# matching
|
||||
acls.permitted?(u1, "/tmp/write/test", Acl::Perm::Read).should be_true
|
||||
acls.permitted?(u1, "/tmp/write/test", Acl::Perm::Write).should be_true
|
||||
acls.permitted?(u1, "/match/write-ok", Acl::Perm::Read).should be_true
|
||||
acls.permitted?(u1, "/match/write-ok", Acl::Perm::Write).should be_true
|
||||
# TODO: enable those tests
|
||||
acls.permitted?(u1, "/match/not-file", Acl::Perm::Write).should be_false
|
||||
acls.permitted?(u1, "/match/not-dir/any", Acl::Perm::Write).should be_false
|
||||
acls.permitted?(u1, "/match/not-file", Acl::Perm::Read).should be_false
|
||||
acls.permitted?(u1, "/match/not-dir/any", Acl::Perm::Read).should be_false
|
||||
end
|
||||
|
||||
it "test the paths matching" do
|
||||
Acl::Path.new("/*").acl_match?("/a/test").should eq(true)
|
||||
Acl::Path.new("/a*").acl_match?("/a/test").should eq(true)
|
||||
Acl::Path.new("/a/test*").acl_match?("/a/test").should eq(true)
|
||||
Acl::Path.new("/a/test*").acl_match?("/a/test/").should eq(true)
|
||||
Acl::Path.new("/a/test*").acl_match?("/b/test").should eq(false)
|
||||
Acl::Path.new("/a/test*").acl_match?("/a/other").should eq(false)
|
||||
end
|
||||
|
||||
it "groups having" do
|
||||
acls = Acl::Groups.new File.tempfile("spec").to_s
|
||||
acls.add "guest"
|
||||
acls.add "admin"
|
||||
acls["guest"]["/*"] = Acl::Perm::Read
|
||||
acls["guest"]["/write/*"] = Acl::Perm::Write
|
||||
acls["guest"]["/write/admin"] = Acl::Perm::Read
|
||||
acls["admin"]["/*"] = Acl::Perm::Write
|
||||
acls.groups_having_any_access_to("/", Acl::Perm::Read).should eq(["guest", "admin"])
|
||||
acls.groups_having_any_access_to("/", Acl::Perm::Write).should eq(["admin"])
|
||||
acls.groups_having_any_access_to("/write", Acl::Perm::Write).should eq(["admin"])
|
||||
acls.groups_having_any_access_to("/write/anypage", Acl::Perm::Write).should eq(["guest", "admin"])
|
||||
acls.groups_having_any_access_to("/write/admin", Acl::Perm::Write).should eq(["admin"])
|
||||
acls.groups_having_direct_access_to("/*", Acl::Perm::Read).should eq(["guest", "admin"])
|
||||
acls.groups_having_direct_access_to("/*", Acl::Perm::Write).should eq(["admin"])
|
||||
end
|
||||
end
|
|
@ -1,114 +0,0 @@
|
|||
describe Acl::Group do
|
||||
it "test initialize" do
|
||||
# First initializer, simple
|
||||
g1 = Acl::Group.new("name1",
|
||||
{Acl::Path.new("/") => Acl::Perm::Read},
|
||||
Acl::Perm::None
|
||||
)
|
||||
# First initializer, named arguments
|
||||
g2 = Acl::Group.new(
|
||||
name: "name2",
|
||||
permissions: {Acl::Path.new("/o") => Acl::Perm::Write},
|
||||
default: Acl::Perm::Read
|
||||
)
|
||||
# Sec initializer, simple
|
||||
g3 = Acl::Group.new("name3",
|
||||
{"/" => Acl::Perm::Read},
|
||||
Acl::Perm::None
|
||||
)
|
||||
# Sec initializer, named arguments
|
||||
g4 = Acl::Group.new(
|
||||
name: "name4",
|
||||
permissions: {"/o" => Acl::Perm::Write},
|
||||
default: Acl::Perm::Read
|
||||
)
|
||||
g1.name.should eq "name1"
|
||||
g2.name.should eq "name2"
|
||||
g3.name.should eq "name3"
|
||||
g4.name.should eq "name4"
|
||||
g1.default.should eq Acl::Perm::None
|
||||
g2.default.should eq Acl::Perm::Read
|
||||
g3.default.should eq Acl::Perm::None
|
||||
g4.default.should eq Acl::Perm::Read
|
||||
end
|
||||
|
||||
it "test permitted?" do
|
||||
g = Acl::Group.new(
|
||||
name: "guest",
|
||||
permissions: {
|
||||
Acl::Path.new("/public*") => Acl::Perm::Write,
|
||||
Acl::Path.new("/restricted*") => Acl::Perm::Read,
|
||||
Acl::Path.new("/users*") => Acl::Perm::Read,
|
||||
Acl::Path.new("/users/guest") => Acl::Perm::Write,
|
||||
Acl::Path.new("/users/protected/*") => Acl::Perm::None,
|
||||
},
|
||||
default: Acl::Perm::None
|
||||
)
|
||||
g.permitted?("/", Acl::Perm::Read).should be_false
|
||||
g.permitted?("/users", Acl::Perm::Read).should be_true
|
||||
g.permitted?("/users/guest", Acl::Perm::Read).should be_true
|
||||
g.permitted?("/users/admin", Acl::Perm::Read).should be_true
|
||||
g.permitted?("/users/guest", Acl::Perm::Write).should be_true
|
||||
g.permitted?("/users/admin", Acl::Perm::Write).should be_false
|
||||
g.permitted?("/users/protected/any", Acl::Perm::Read).should be_false
|
||||
g.permitted?("/public", Acl::Perm::Read).should be_true
|
||||
g.permitted?("/public", Acl::Perm::Write).should be_true
|
||||
g.permitted?("/public/some", Acl::Perm::Write).should be_true
|
||||
g.permitted?("/public/some", Acl::Perm::Write).should be_true
|
||||
g.permitted?("/publicXXX/some", Acl::Perm::Write).should be_true
|
||||
end
|
||||
|
||||
it "advanced test permitted?" do
|
||||
g = Acl::Group.new(
|
||||
name: "guest",
|
||||
permissions: {
|
||||
Acl::Path.new("/write/*") => Acl::Perm::Write,
|
||||
Acl::Path.new("/read/*") => Acl::Perm::Read,
|
||||
Acl::Path.new("/none/*") => Acl::Perm::None,
|
||||
Acl::Path.new("/read/none") => Acl::Perm::None,
|
||||
Acl::Path.new("/write/none") => Acl::Perm::None,
|
||||
Acl::Path.new("/none/readonly") => Acl::Perm::Read,
|
||||
Acl::Path.new("/write/readonly") => Acl::Perm::Read,
|
||||
Acl::Path.new("/read/writeonly") => Acl::Perm::Write,
|
||||
Acl::Path.new("/none/writeonly") => Acl::Perm::Write,
|
||||
},
|
||||
default: Acl::Perm::Read
|
||||
)
|
||||
g.permitted?("/", Acl::Perm::Read).should be_true
|
||||
g.permitted?("/", Acl::Perm::Write).should be_false
|
||||
g.permitted?("/none/any", Acl::Perm::None).should be_true
|
||||
g.permitted?("/none/any", Acl::Perm::Read).should be_false
|
||||
g.permitted?("/read/any", Acl::Perm::Read).should be_true
|
||||
g.permitted?("/read/any", Acl::Perm::Write).should be_false
|
||||
g.permitted?("/write/any", Acl::Perm::Write).should be_true
|
||||
g.permitted?("/read/none", Acl::Perm::Read).should be_false
|
||||
g.permitted?("/write/none", Acl::Perm::Read).should be_false
|
||||
g.permitted?("/none/readonly", Acl::Perm::Read).should be_true
|
||||
g.permitted?("/write/readonly", Acl::Perm::Write).should be_false
|
||||
g.permitted?("/none/writeonly", Acl::Perm::Write).should be_true
|
||||
g.permitted?("/read/writeonly", Acl::Perm::Write).should be_true
|
||||
end
|
||||
|
||||
it "test permissions access" do
|
||||
g = Acl::Group.new(
|
||||
name: "guest",
|
||||
permissions: {
|
||||
"/public*" => Acl::Perm::Write,
|
||||
"/restricted*" => Acl::Perm::Read,
|
||||
"/users*" => Acl::Perm::Read,
|
||||
"/users/guest" => Acl::Perm::Write,
|
||||
},
|
||||
default: Acl::Perm::None
|
||||
)
|
||||
g["/"]?.should eq nil
|
||||
g["/"].should eq g.default
|
||||
# TODO: enable this test after changing the design of operator [] to do not match path
|
||||
# g["/public"]?.should eq nil
|
||||
g["/public*"]?.should eq Acl::Perm::Write
|
||||
|
||||
g["/"] = Acl::Perm::Read
|
||||
g["/"]?.should eq Acl::Perm::Read
|
||||
g.delete("/")
|
||||
g["/"]?.should eq nil
|
||||
end
|
||||
end
|
|
@ -1,26 +0,0 @@
|
|||
describe Wikicr::MarkdPatch do
|
||||
page = Wikicr::Page.new "", real_url: false
|
||||
index = Wikicr::Page::Index.new "/tmp/specs.index.yaml"
|
||||
|
||||
it "basic markd patching" do
|
||||
raw = "hello <<test1>>"
|
||||
html = %Q{<p>hello <a href="/test1" title="test1">test1</a></p>\n}
|
||||
Wikicr::MarkdPatch.to_html(raw, page, index).should eq(html)
|
||||
end
|
||||
|
||||
it "wiki tag markd patching" do
|
||||
raw = "hello {{test2}}"
|
||||
html = %Q{<p>hello <a href="/test2" title="test2">test2</a></p>\n}
|
||||
Wikicr::MarkdPatch.to_html(raw, page, index).should eq(html)
|
||||
|
||||
raw = "hello {{title 2|test2}}"
|
||||
html = %Q{<p>hello <a href="/test2" title="title 2">title 2</a></p>\n}
|
||||
Wikicr::MarkdPatch.to_html(raw, page, index).should eq(html)
|
||||
end
|
||||
|
||||
it "section anchors" do
|
||||
raw = "# titleA\n1234567890\n## titleB"
|
||||
html = %Q{<h1><a id="anchor-titleA" class="anchor" href="#anchor-titleA"></a>titleA</h1>\n<p>1234567890</p>\n<h2><a id="anchor-titleB" class="anchor" href="#anchor-titleB"></a>titleB</h2>\n}
|
||||
Wikicr::MarkdPatch.to_html(raw, page, index).should eq(html)
|
||||
end
|
||||
end
|
9
spec/mdwikiface_spec.cr
Normal file
9
spec/mdwikiface_spec.cr
Normal file
|
@ -0,0 +1,9 @@
|
|||
require "./spec_helper"
|
||||
|
||||
describe Wikicr do
|
||||
# TODO: Write tests
|
||||
|
||||
it "works" do
|
||||
false.should eq(true)
|
||||
end
|
||||
end
|
|
@ -1,64 +0,0 @@
|
|||
page = Wikicr::Page.new ""
|
||||
|
||||
describe Wikicr::Page::Index do
|
||||
it "one by title or url" do
|
||||
index = Wikicr::Page::Index.new(file: "placeholder")
|
||||
|
||||
index.entries["page1"] = Wikicr::Page::Index::Entry.new path: "path1", url: "url1", title: "title1"
|
||||
index.entries["page2"] = Wikicr::Page::Index::Entry.new path: "path2", url: "url2", title: "title2"
|
||||
index.entries["page3"] = Wikicr::Page::Index::Entry.new path: "path3", url: "url3", title: "Title3"
|
||||
index.entries["page4"] = Wikicr::Page::Index::Entry.new path: "path4", url: "url4", title: "Title3"
|
||||
|
||||
title2 = index.one_by_title_or_url text: "title2", context: page
|
||||
title2.should eq index.entries["page2"]
|
||||
|
||||
url2 = index.one_by_title_or_url text: "url2", context: page
|
||||
url2.should eq index.entries["page2"]
|
||||
|
||||
urlnil = index.one_by_title_or_url text: "urlnil", context: page
|
||||
index.entries.each { |_, entry| urlnil.should_not eq(entry) }
|
||||
end
|
||||
|
||||
it "all by tags" do
|
||||
index = Wikicr::Page::Index.new(file: "placeholder")
|
||||
index.entries["entry1"] = Wikicr::Page::Index::Entry.new(
|
||||
path: "path 1",
|
||||
url: "url 1",
|
||||
title: "title 1",
|
||||
tags: ["tag1", "tag12", "tag13", "tag14"]
|
||||
)
|
||||
index.entries["entry2"] = Wikicr::Page::Index::Entry.new(
|
||||
path: "path 2",
|
||||
url: "url 2",
|
||||
title: "title 2",
|
||||
tags: ["tag12"]
|
||||
)
|
||||
index.entries["entry3"] = Wikicr::Page::Index::Entry.new(
|
||||
path: "path 3",
|
||||
url: "url 3",
|
||||
title: "title 3",
|
||||
tags: ["tag13", "tag134"]
|
||||
)
|
||||
index.entries["entry4"] = Wikicr::Page::Index::Entry.new(
|
||||
path: "path 4",
|
||||
url: "url 4",
|
||||
title: "title 4",
|
||||
tags: ["tag14", "tag134"]
|
||||
)
|
||||
|
||||
expected = {"entry1" => index.entries["entry1"]}
|
||||
index.all_by_tags(tags_line: "tag1", context: page).should eq(expected)
|
||||
|
||||
expected = {"entry1" => index.entries["entry1"], "entry2" => index.entries["entry2"]}
|
||||
index.all_by_tags(tags_line: "tag12", context: page).should eq(expected)
|
||||
|
||||
expected = {"entry2" => index.entries["entry2"]}
|
||||
index.all_by_tags(tags_line: "tag12 -tag1", context: page).should eq(expected)
|
||||
|
||||
expected = {"entry1" => index.entries["entry1"]}
|
||||
index.all_by_tags(tags_line: "tag12 +tag13", context: page).should eq(expected)
|
||||
|
||||
expected = {"entry1" => index.entries["entry1"], "entry2" => index.entries["entry2"], "entry3" => index.entries["entry3"]}
|
||||
index.all_by_tags(tags_line: "tag12 tag13", context: page).should eq(expected)
|
||||
end
|
||||
end
|
|
@ -1,15 +0,0 @@
|
|||
describe Wikicr::Page do
|
||||
it "test basic" do
|
||||
page = Wikicr::Page.new(url: "home", real_url: false, parse_title: false)
|
||||
page.url.should eq "home"
|
||||
page.real_url.should eq "/pages/home"
|
||||
page.path.should eq File.expand_path("data/home.md")
|
||||
end
|
||||
|
||||
it "test another basic" do
|
||||
page = Wikicr::Page.new(url: "/pages/home", real_url: true, parse_title: false)
|
||||
page.url.should eq "home"
|
||||
page.real_url.should eq "/pages/home"
|
||||
page.path.should eq File.expand_path("data/home.md")
|
||||
end
|
||||
end
|
|
@ -1,10 +1,2 @@
|
|||
require "spec"
|
||||
|
||||
require "../src/lib/lockable"
|
||||
require "../src/lib/options"
|
||||
|
||||
require "../src/lib/file_tree"
|
||||
require "../src/lib/acl"
|
||||
require "../src/lib/page"
|
||||
require "../src/lib/wikimd/**"
|
||||
require "../src/lib/users/**"
|
||||
require "../src/wikicr"
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
require "./spec_helper"
|
||||
|
||||
require "./acl_spec"
|
||||
require "./group_spec"
|
||||
require "./markdown_spec"
|
||||
require "./page_spec"
|
14
src/controllers.cr
Normal file
14
src/controllers.cr
Normal file
|
@ -0,0 +1,14 @@
|
|||
macro new_render(name)
|
||||
macro render_{{name.id}}(page)
|
||||
render {{ "src/views/" + name.stringify + "s/\{\{page}}.html.slang" }}, "src/views/layout.html.slang"
|
||||
end
|
||||
end
|
||||
|
||||
new_render(page)
|
||||
new_render(user)
|
||||
|
||||
# macro render_page(page)
|
||||
# render {{ "src/views/pages/" + page + ".html.slang" }}, "src/views/layout.html.slang"
|
||||
# end
|
||||
|
||||
require "./controllers/*"
|
|
@ -1,85 +0,0 @@
|
|||
require "./application_controller"
|
||||
|
||||
class AdminController < ApplicationController
|
||||
# get /admin/users
|
||||
def users_show
|
||||
acl_permit! :write
|
||||
users = Wikicr::USERS.load!
|
||||
render "users_show.slang"
|
||||
end
|
||||
|
||||
# post /admin/users/create
|
||||
def user_create
|
||||
acl_permit! :write
|
||||
username = params.body["username"]
|
||||
password = params.body["password"]
|
||||
groups = params.body["groups"].split(",").map(&.strip)
|
||||
begin
|
||||
user = Wikicr::USERS.register! username, password, groups
|
||||
flash["success"] = "The user #{user.name} has been added."
|
||||
rescue err
|
||||
flash["danger"] = "Cannot register this account: #{err.message}."
|
||||
end
|
||||
redirect_to "/admin/users"
|
||||
end
|
||||
|
||||
# post /admin/users/delete
|
||||
def user_delete
|
||||
acl_permit! :write
|
||||
Wikicr::USERS.transaction! { |users| users.delete params.body["username"] }
|
||||
flash["success"] = "The user #{params.body["username"]} has been deleted."
|
||||
redirect_to "/admin/users"
|
||||
end
|
||||
|
||||
# get /admin/acls
|
||||
def acls_show
|
||||
acl_permit! :read
|
||||
acls = Wikicr::ACL.load!
|
||||
render "acls_show.slang"
|
||||
end
|
||||
|
||||
# post /admin/acls/create
|
||||
def acl_create
|
||||
acl_permit! :write
|
||||
group = params.body["group"]
|
||||
path = params.body["path"]
|
||||
perm_str = params.body["perm"]
|
||||
perm = Acl::PERM_STR[perm_str]
|
||||
Wikicr::ACL.transaction! do |acls|
|
||||
acls.add Acl::Group.new(group) if acls[group]?.nil?
|
||||
acls[group][path] = perm
|
||||
end
|
||||
flash["success"] = "ACL #{group} :: #{path} :: #{perm} has been added"
|
||||
redirect_to "/admin/acls"
|
||||
end
|
||||
|
||||
# post /admin/acls/update
|
||||
def acl_update
|
||||
acl_permit! :write
|
||||
begin
|
||||
group = params.body["group"]
|
||||
path = params.body["path"]
|
||||
perm_str = params.body["change"]
|
||||
perm = Acl::PERM_STR[perm_str]
|
||||
Wikicr::ACL.transaction! do |acls|
|
||||
acls[group][path] = perm
|
||||
end
|
||||
flash["success"] = "ACL #{group} :: #{path} :: #{perm} has been updated."
|
||||
rescue err
|
||||
flash["danger"] = "Unable to process that: #{err.message}."
|
||||
end
|
||||
redirect_to "/admin/acls"
|
||||
end
|
||||
|
||||
# post /admin/acls/delete
|
||||
def acl_delete
|
||||
acl_permit! :write
|
||||
group = params.body["group"]
|
||||
path = params.body["path"]
|
||||
Wikicr::ACL.transaction! do |acls|
|
||||
acls[group].delete path
|
||||
end
|
||||
flash["success"] = "ACL #{group} :: #{path} has been deleted."
|
||||
redirect_to "/admin/acls"
|
||||
end
|
||||
end
|
|
@ -1,31 +0,0 @@
|
|||
# This files creates the main controller which is inherited by any other controller.
|
||||
# It also loads the controller and helpers.
|
||||
|
||||
require "http"
|
||||
require "./application_controller/**"
|
||||
require "./helpers/**"
|
||||
|
||||
# The ApplicationController is the class that handles the environment:
|
||||
# it handles the session, request, response, params, flash notices, cookies, redirections, and rendering.
|
||||
class ApplicationController
|
||||
LAYOUT = "application.slang"
|
||||
include ApplicationController::Render
|
||||
include ApplicationController::Session
|
||||
include ApplicationController::Request
|
||||
include ApplicationController::Response
|
||||
include ApplicationController::Params
|
||||
include ApplicationController::Flash
|
||||
include ApplicationController::Cookies
|
||||
include ApplicationController::Redirect
|
||||
|
||||
include Wikicr::Helpers::User
|
||||
include Wikicr::Helpers::Page
|
||||
include Wikicr::Helpers::History
|
||||
|
||||
getter env : HTTP::Server::Context
|
||||
|
||||
def initialize(@env)
|
||||
end
|
||||
end
|
||||
|
||||
require "./**"
|
|
@ -1,13 +0,0 @@
|
|||
class ApplicationController
|
||||
module Cookies
|
||||
delegate :cookies, to: @env.response
|
||||
|
||||
def set_cookie(**cookie)
|
||||
cookies << HTTP::Cookie.new(**cookie)
|
||||
end
|
||||
|
||||
def delete_cookie(name)
|
||||
set_cookie(name: name, value: "", expires: Time.local)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
class ApplicationController
|
||||
module Flash
|
||||
delegate :flash, to: @env
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
class ApplicationController
|
||||
module Params
|
||||
delegate :params, to: @env
|
||||
end
|
||||
end
|
|
@ -1,8 +0,0 @@
|
|||
class ApplicationController
|
||||
module Redirect
|
||||
# TODO: improve that
|
||||
def redirect_to(path, *args_to_hanle, **stuff_to_handle)
|
||||
@env.redirect path
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,52 +0,0 @@
|
|||
class ApplicationController
|
||||
# This code belongs to the Amber Project: https://github.com/Amber-Crystal/amber/blob/master/src/amber/controller/render.cr
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2017 Elias Perez
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
module Render
|
||||
macro render_template(filename, path = "src/views")
|
||||
{% if filename.id.split("/").size > 2 %}
|
||||
Kilt.render("{{filename.id}}")
|
||||
{% else %}
|
||||
Kilt.render("#{{{path}}}/{{filename.id}}")
|
||||
{% end %}
|
||||
end
|
||||
|
||||
macro render(filename, layout = true, path = "src/views", folder = __FILE__)
|
||||
# NOTE: content is basically yield rails layouts.
|
||||
{% if filename.id.split("/").size > 1 %}
|
||||
content = render_template("#{{{filename}}}", {{path}})
|
||||
{% else %}
|
||||
{% if folder.id.ends_with?(".ecr") %}
|
||||
content = render_template("#{{{folder.split("/")[-2]}}}/#{{{filename}}}", {{path}})
|
||||
{% else %}
|
||||
content = render_template("#{{{folder.split("/").last.gsub(/\_controller\.cr|\.cr/, "")}}}/#{{{filename}}}", {{path}})
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
{% if layout && !filename.id.split("/").last.starts_with?("_") %}
|
||||
content = render_template("layouts/#{{{layout.class_name == "StringLiteral" ? layout : LAYOUT}}}", {{path}})
|
||||
{% end %}
|
||||
content
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
class ApplicationController
|
||||
module Request
|
||||
delegate :request, to: @env
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
class ApplicationController
|
||||
module Response
|
||||
delegate :response, to: @env
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
class ApplicationController
|
||||
module Session
|
||||
delegate :session, to: @env
|
||||
end
|
||||
end
|
15
src/controllers/errors.cr
Normal file
15
src/controllers/errors.cr
Normal file
|
@ -0,0 +1,15 @@
|
|||
get "/" do |env|
|
||||
env.redirect "/pages/"
|
||||
end
|
||||
|
||||
get "/*" do |env|
|
||||
env.redirect "/pages/"
|
||||
end
|
||||
|
||||
error 404 do |env|
|
||||
"Page not found"
|
||||
end
|
||||
|
||||
error 403 do
|
||||
"Forbidden"
|
||||
end
|
|
@ -1,44 +0,0 @@
|
|||
module Wikicr::Helpers::History
|
||||
class HistoryStorage < Array(String)
|
||||
SEPARATOR = "|"
|
||||
KEEP_ENTRIES = 6
|
||||
|
||||
@app : ApplicationController
|
||||
|
||||
def initialize(@app)
|
||||
super(1)
|
||||
end
|
||||
|
||||
def parse(history_string : String) : self
|
||||
history_string.split(SEPARATOR).each do |page|
|
||||
push(page)
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def <<(page : Wikicr::Page) : self
|
||||
push(page.real_url)
|
||||
uniq!
|
||||
shift_amount = size - KEEP_ENTRIES
|
||||
shift(shift_amount) if shift_amount > 0
|
||||
|
||||
@app.set_cookie name: "user.history", value: URI.encode(to_s), expires: 14.days.from_now, path: "/pages"
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def to_s
|
||||
join(SEPARATOR)
|
||||
end
|
||||
end
|
||||
|
||||
def history : HistoryStorage
|
||||
current_history = cookies["user.history"]?
|
||||
if current_history
|
||||
HistoryStorage.new(self).parse(URI.decode(current_history.value))
|
||||
else
|
||||
HistoryStorage.new(self)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,42 +0,0 @@
|
|||
module Wikicr::Helpers::Page
|
||||
# TODO: move that
|
||||
def add_page(page, stack = [] of String)
|
||||
String.build do |str|
|
||||
Slang.embed("src/views/pages/sitemap.directory.slang", "str")
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: move that
|
||||
def create_toc_line(line, current_id, ends = true)
|
||||
"<li><a href=\"#anchor-#{line}\">#{line}</a>#{ends ? "</li>" : nil}\n"
|
||||
end
|
||||
|
||||
# TODO: move that
|
||||
def add_toc_level(b, index_entry, current_id = 0, last_head = 0)
|
||||
return if index_entry.size == current_id
|
||||
current_entry = index_entry[current_id]
|
||||
current_head = current_entry[0]
|
||||
current_head_value = current_entry[1]
|
||||
next_entry = index_entry[current_id + 1]?
|
||||
next_head = next_entry ? next_entry[0] : 7
|
||||
close_li = next_head <= current_head
|
||||
if current_head > last_head
|
||||
b << "<ol>\n" << create_toc_line(current_head_value, current_id, close_li)
|
||||
elsif current_head < last_head
|
||||
b << "</ol></li>\n" << create_toc_line(current_head_value, current_id, close_li)
|
||||
else
|
||||
b << create_toc_line(current_head_value, current_id, close_li)
|
||||
end
|
||||
return add_toc_level(b, index_entry, current_id + 1, current_head)
|
||||
end
|
||||
|
||||
def add_toc(index_entry)
|
||||
# (index_entry.values.map(&.size).sum + index_entry.size * 9)
|
||||
toc = String.build do |b|
|
||||
add_toc_level(b, index_entry)
|
||||
end
|
||||
String.build do |str|
|
||||
Slang.embed("src/views/pages/toc.slang", "str")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,57 +0,0 @@
|
|||
module Wikicr::Helpers::User
|
||||
# The username+token are set into the cookies in order to allow future auto-login
|
||||
def set_login_cookies_for(username)
|
||||
Wikicr::USERS.transaction!(read: true) { |users| users[username].generate_new_token! }
|
||||
token = Wikicr::USERS[username].token.to_s
|
||||
set_cookie name: "user.name", value: username, expires: 14.days.from_now
|
||||
set_cookie name: "user.token", value: token, expires: 14.days.from_now
|
||||
end
|
||||
|
||||
# If a cookie is set but the user is not signed in, try to use it and renew the cookie
|
||||
def uses_login_cookies
|
||||
unless user_signed_in?
|
||||
if (name = cookies["user.name"]?) && (token = cookies["user.token"]?)
|
||||
if (user = Wikicr::USERS.auth_token?(name.value, token.value))
|
||||
session.string("user.name", user.name)
|
||||
set_login_cookies_for(user.name)
|
||||
else
|
||||
puts "Invalid cookies creditentials"
|
||||
delete_cookie "user.name"
|
||||
delete_cookie "user.token"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Nil if not signed in, else it returns the user name
|
||||
macro user_signed_in?
|
||||
session.string?("user.name")
|
||||
end
|
||||
|
||||
# Nil if not signed in, else it returns the user name
|
||||
macro user_name?
|
||||
session.string?("user.name")
|
||||
end
|
||||
|
||||
# If the user is connected return an `Wikicr::User`, else the default user (guest)
|
||||
macro current_user
|
||||
%name = user_signed_in?
|
||||
if (%name.nil?)
|
||||
Wikicr::USERS.default || raise "User not signed in"
|
||||
else
|
||||
Wikicr::USERS.find(%name)
|
||||
end
|
||||
end
|
||||
|
||||
macro acl_permit!(perm)
|
||||
uses_login_cookies
|
||||
if Wikicr::ACL.permitted?(current_user, request.path, Acl::PERM[{{perm}}])
|
||||
puts "PERMITTED #{current_user.name} #{request.path} #{Acl::PERM[{{perm}}]}"
|
||||
else
|
||||
puts "NOT PERMITTED #{current_user.name} #{request.path} #{Acl::PERM[{{perm}}]}"
|
||||
flash["danger"] = "You are not permitted to access this resource (#{request.path}, #{{{perm}}})."
|
||||
redirect_to "/pages/home"
|
||||
return # Stop the action
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,12 +0,0 @@
|
|||
require "./application_controller"
|
||||
|
||||
class HomeController < ApplicationController
|
||||
# get /
|
||||
def index
|
||||
if Wikicr::ACL.permitted?(current_user, "/pages/home", Acl::Perm::Read)
|
||||
redirect_to "/pages/home"
|
||||
else
|
||||
"Not authorized"
|
||||
end
|
||||
end
|
||||
end
|
52
src/controllers/pages.cr
Normal file
52
src/controllers/pages.cr
Normal file
|
@ -0,0 +1,52 @@
|
|||
require "markdown"
|
||||
|
||||
before_all do |env|
|
||||
# env.session = Session.new(env.cookies)
|
||||
end
|
||||
|
||||
private def fetch_params(env)
|
||||
path = env.params.url["path"]
|
||||
{
|
||||
path: path, # basic path from the params unmodified
|
||||
display_path: path, # TODO: clean the path
|
||||
title: path.split("/").last, # keep only the name of the file
|
||||
page: Wikicr::Page.new(path), # page handler
|
||||
}
|
||||
# file_path: Wikicr::Page.new(path).jail.file, # jail the path (safety)
|
||||
end
|
||||
|
||||
get "/pages/search" do |env|
|
||||
user_must_be_logged!(env)
|
||||
query = env.params.query["q"]
|
||||
# TODO: a real search
|
||||
env.redirect query.empty? ? "/pages" : query
|
||||
end
|
||||
|
||||
get "/pages/" do |env|
|
||||
user_must_be_logged!(env)
|
||||
env.redirect("/pages/home")
|
||||
end
|
||||
|
||||
get "/pages/*path" do |env|
|
||||
user_must_be_logged!(env)
|
||||
locals = fetch_params(env).to_h
|
||||
locals[:body] = (locals[:page].as(Wikicr::Page).read(current_user(env)) rescue "")
|
||||
if (env.params.query["edit"]?) || !locals[:page].as(Wikicr::Page).exists?(current_user(env))
|
||||
render_page(edit)
|
||||
else
|
||||
locals[:body_html] = Markdown.to_html(locals[:body].as(String))
|
||||
render_page(show)
|
||||
end
|
||||
end
|
||||
|
||||
post "/pages/*path" do |env|
|
||||
user_must_be_logged!(env)
|
||||
locals = fetch_params(env).to_h
|
||||
if (env.params.body["body"]?.to_s.empty?)
|
||||
locals[:page].as(Wikicr::Page).delete(current_user(env)) rescue nil
|
||||
env.redirect "/pages/"
|
||||
else
|
||||
locals[:page].as(Wikicr::Page).write env.params.body["body"], current_user(env)
|
||||
env.redirect "/pages/#{locals[:path]}"
|
||||
end
|
||||
end
|
|
@ -1,109 +0,0 @@
|
|||
require "./application_controller"
|
||||
require "../lib/wikimd/*"
|
||||
|
||||
class PagesController < ApplicationController
|
||||
# get /sitemap
|
||||
def sitemap
|
||||
acl_permit! :read
|
||||
pages = Wikicr::FileTree.build Wikicr::OPTIONS.basedir
|
||||
render "sitemap.slang"
|
||||
end
|
||||
|
||||
# get /pages/search?q=
|
||||
def search
|
||||
query = params.query["q"]
|
||||
page = Wikicr::Page.new(query)
|
||||
# TODO: a real search
|
||||
redirect_to query.empty? ? "/pages/home" : page.real_url
|
||||
end
|
||||
|
||||
# get /pages/*path
|
||||
def show
|
||||
acl_permit! :read
|
||||
flash["danger"] = params.query["flash.danger"] if params.query["flash.danger"]?
|
||||
page = Wikicr::Page.new url: params.url["path"], parse_title: true
|
||||
if (params.query["edit"]?) || !page.exists?
|
||||
show_edit(page)
|
||||
else
|
||||
show_show(page)
|
||||
end
|
||||
end
|
||||
|
||||
private def show_edit(page)
|
||||
body = page.read rescue ""
|
||||
flash["info"] = "The page #{page.url} does not exist yet." if !page.exists?
|
||||
acl_permit! :write
|
||||
render "edit.slang"
|
||||
end
|
||||
|
||||
private def show_show(page)
|
||||
index = Wikicr::PAGES.load!
|
||||
body_html = Wikicr::MarkdPatch.to_html(input: page.read, index: index, context: page)
|
||||
|
||||
groups_read = Wikicr::ACL.groups_having_any_access_to page.real_url, Acl::Perm::Read, true
|
||||
groups_write = Wikicr::ACL.groups_having_any_access_to page.real_url, Acl::Perm::Write, true
|
||||
history << page
|
||||
render "show.slang"
|
||||
end
|
||||
|
||||
# post /pages/*path
|
||||
def update
|
||||
acl_permit! :write
|
||||
page = Wikicr::Page.new url: params.url["path"], parse_title: true
|
||||
if params.body["rename"]?
|
||||
update_rename(page)
|
||||
elsif (params.body["body"]?.to_s.empty?)
|
||||
update_delete(page)
|
||||
else
|
||||
update_edit(page)
|
||||
end
|
||||
end
|
||||
|
||||
private def update_rename(page)
|
||||
if !params.body["new_path"]?.to_s.strip.empty?
|
||||
# TODO: verify if the user can write on new_path
|
||||
# TODO: if new_path do not begin with /, relative rename to the current path
|
||||
renamed_page = page # will be change in the transaction
|
||||
Wikicr::PAGES.transaction! do |index|
|
||||
index.delete page
|
||||
renamed_page = page.rename current_user, params.body["new_path"]
|
||||
renamed_page.parse_tags! index
|
||||
index.add renamed_page
|
||||
end
|
||||
flash["success"] = "The page #{page.url} has been moved to #{renamed_page.url}."
|
||||
redirect_to renamed_page.real_url # "/pages/#{params.body["new_path"]}"
|
||||
else
|
||||
redirect_to page.real_url
|
||||
end
|
||||
end
|
||||
|
||||
private def update_delete(page)
|
||||
begin
|
||||
Wikicr::PAGES.transaction! { |index| index.delete page }
|
||||
page.delete current_user
|
||||
flash["success"] = "The page #{page.url} has been deleted."
|
||||
redirect_to "/pages/home"
|
||||
rescue err
|
||||
# TODO: what if the page is not deleted but not indexed anymore ?
|
||||
# Wikicr::PAGES.transaction! { |index| index.add page }
|
||||
flash["danger"] = "Error: cannot remove #{page.url}, #{err.message}"
|
||||
redirect_to page.real_url
|
||||
end
|
||||
end
|
||||
|
||||
private def update_edit(page)
|
||||
begin
|
||||
page.write current_user, params.body["body"]
|
||||
page.parse_title!
|
||||
Wikicr::PAGES.transaction! do |index|
|
||||
page.parse_tags! index
|
||||
index.add page
|
||||
end
|
||||
flash["success"] = "The page #{page.url} has been updated."
|
||||
redirect_to page.real_url
|
||||
rescue err
|
||||
flash["danger"] = "Error: cannot update #{page.url}, #{err.message}"
|
||||
redirect_to page.real_url
|
||||
end
|
||||
end
|
||||
end
|
10
src/controllers/sitemap.cr
Normal file
10
src/controllers/sitemap.cr
Normal file
|
@ -0,0 +1,10 @@
|
|||
def add_page(page, stack = [] of String)
|
||||
String.build do |str|
|
||||
Slang.embed("src/views/sitemap.directory.html.slang", "str")
|
||||
end
|
||||
end
|
||||
|
||||
get "/sitemap" do |env|
|
||||
locals = {title: "sitemap", pages: Wikicr::SFile.build(Wikicr::OPTIONS.basedir)}
|
||||
render "src/views/sitemap.html.slang", "src/views/layout.html.slang"
|
||||
end
|
45
src/controllers/users.cr
Normal file
45
src/controllers/users.cr
Normal file
|
@ -0,0 +1,45 @@
|
|||
module Wikicr
|
||||
Dir.mkdir_p("meta")
|
||||
File.touch("meta/users")
|
||||
USERS = Wikicr::Users.new("meta/users")
|
||||
end
|
||||
|
||||
private def fetch_params(env)
|
||||
{
|
||||
username: (env.params.body["username"]?),
|
||||
password: (env.params.body["password"]?),
|
||||
}
|
||||
end
|
||||
|
||||
get "/users/login" do |env|
|
||||
locals = {title: "Login"}
|
||||
render_user(login)
|
||||
end
|
||||
|
||||
post "/users/login" do |env|
|
||||
locals = fetch_params(env)
|
||||
user = Wikicr::USERS.auth! locals[:username].to_s, locals[:password].to_s
|
||||
# TODO: make a notification
|
||||
if user.nil?
|
||||
env.redirect "/users/login"
|
||||
else
|
||||
env.session.string("username", user.name)
|
||||
env.redirect "/pages"
|
||||
end
|
||||
end
|
||||
|
||||
get "/users/register" do |env|
|
||||
locals = {title: "Register"}
|
||||
render_user(register)
|
||||
end
|
||||
|
||||
post "/users/register" do |env|
|
||||
locals = fetch_params(env)
|
||||
# TODO: make a notification
|
||||
begin
|
||||
user = Wikicr::USERS.register! locals[:username].to_s, locals[:password].to_s
|
||||
env.redirect "/users/login"
|
||||
rescue
|
||||
env.redirect "/users/register"
|
||||
end
|
||||
end
|
|
@ -1,45 +0,0 @@
|
|||
require "./application_controller"
|
||||
|
||||
class UsersController < ApplicationController
|
||||
# get /users/login
|
||||
def login
|
||||
acl_permit! :read
|
||||
render "login.slang"
|
||||
end
|
||||
|
||||
# post /users/login
|
||||
def login_validates
|
||||
acl_permit! :write
|
||||
user = Wikicr::USERS.auth! params.body["username"].to_s, params.body["password"].to_s
|
||||
# TODO: make a notification
|
||||
if user.nil?
|
||||
flash["danger"] = "User or password doesn't match."
|
||||
redirect_to "/users/login"
|
||||
else
|
||||
flash["success"] = "You are connected!"
|
||||
session.string("user.name", user.name)
|
||||
set_login_cookies_for(user.name)
|
||||
redirect_to "/pages/home"
|
||||
end
|
||||
end
|
||||
|
||||
# get /users/register
|
||||
def register
|
||||
acl_permit! :read
|
||||
render "register.slang"
|
||||
end
|
||||
|
||||
# post /users/register
|
||||
def register_validates
|
||||
acl_permit! :write
|
||||
# TODO: make a notification
|
||||
begin
|
||||
user = Wikicr::USERS.register! params.body["username"].to_s, params.body["password"].to_s
|
||||
flash["success"] = "You are registrated under the username #{user.name}. You can connect now."
|
||||
redirect_to "/users/login"
|
||||
rescue err
|
||||
flash["danger"] = "Cannot register this account: #{err.message}."
|
||||
redirect_to "/users/register"
|
||||
end
|
||||
end
|
||||
end
|
2
src/lib.cr
Normal file
2
src/lib.cr
Normal file
|
@ -0,0 +1,2 @@
|
|||
require "./lib/options"
|
||||
require "./lib/*"
|
|
@ -1,50 +0,0 @@
|
|||
require "./options"
|
||||
require "./git"
|
||||
|
||||
require "./lockable"
|
||||
require "./errors"
|
||||
|
||||
require "./**"
|
||||
|
||||
# This file initialize the state of the wiki, which is stored into the main module Wikicr
|
||||
# It may need to execute an initialisation to generate default values for `Acl::Groups`, `Wikicr::Users`, `Page::Index`
|
||||
module Wikicr
|
||||
# The dir *meta* contains users account (with encrypted password),
|
||||
# the index of the pages (with table of content, links, titles, ...)
|
||||
# and user permissions
|
||||
Dir.mkdir_p "meta"
|
||||
|
||||
# Define a default user that should be used for anonymous clients
|
||||
DEFAULT_USER = Wikicr::User.new "guest", "guest", %w(guest)
|
||||
|
||||
# The list of the users is stored into *meta/users*. This file is updated when
|
||||
# an user is created/modified/deleted, but the data are stored into RAM for
|
||||
# reading.
|
||||
USERS = Wikicr::Users.new("meta/users", DEFAULT_USER).load!
|
||||
|
||||
# The list of the permissions (group => path+permission) is stored into the
|
||||
# file *meta/acl. Similar behaviour than `USERS`.
|
||||
ACL = Acl::Groups.new("meta/acl").load!
|
||||
|
||||
# If there is no "guest", we assume that the ACL have not been initialized yet
|
||||
# and we create a group "guest" and "user".
|
||||
# TODO: a proper "installation" procedure should be made to avoid these kind
|
||||
# of operation in a scope
|
||||
if ACL["guest"]?.nil?
|
||||
ACL.add("guest")
|
||||
ACL["guest"]["/users/*"] = Acl::Perm::Write
|
||||
ACL["guest"]["/sitemap"] = Acl::Perm::Read
|
||||
ACL["guest"]["/pages*"] = Acl::Perm::Read
|
||||
ACL["guest"]["/"] = Acl::Perm::Read
|
||||
ACL.add("user")
|
||||
ACL["user"]["/*"] = Acl::Perm::Read
|
||||
ACL["user"]["/users/login"] = Acl::Perm::None
|
||||
ACL["user"]["/users/register"] = Acl::Perm::None
|
||||
ACL["user"]["/pages*"] = Acl::Perm::Write
|
||||
ACL.save!
|
||||
end
|
||||
|
||||
# The list of the pages (index) with a lot of meta-data. Same behaviour than
|
||||
# `USERS` and `ACL`.
|
||||
PAGES = Wikicr::Page::Index.new("meta/index").load!
|
||||
end
|
|
@ -1,15 +0,0 @@
|
|||
require "./acl/**"
|
||||
|
||||
module Acl
|
||||
PERM = {
|
||||
none: Acl::Perm::None,
|
||||
read: Acl::Perm::Read,
|
||||
write: Acl::Perm::Write,
|
||||
}
|
||||
|
||||
PERM_STR = {
|
||||
"none" => Acl::Perm::None,
|
||||
"read" => Acl::Perm::Read,
|
||||
"write" => Acl::Perm::Write,
|
||||
}
|
||||
end
|
|
@ -1,11 +0,0 @@
|
|||
require "./perm"
|
||||
require "./group"
|
||||
|
||||
# Entity that have access to the Acl system.
|
||||
module Acl::Entity
|
||||
# Returns `true` if the *group* is owned by the `Entity`, else `false`
|
||||
abstract def has_group?(group : String) : Bool
|
||||
|
||||
# Returns the list of the group names of the `Entity`
|
||||
abstract def groups : Array(String)
|
||||
end
|
|
@ -1,108 +0,0 @@
|
|||
require "yaml"
|
||||
|
||||
require "./perm"
|
||||
require "./path"
|
||||
|
||||
# The Group is identified by a *name* and has *permissions* on a set of paths.
|
||||
# It is used by `Groups`.
|
||||
# NOTE: I did not used `Hash().new(default)` because it is annoying with passing the permissions in the constructor
|
||||
class Acl::Group
|
||||
include YAML::Serializable
|
||||
|
||||
property name : String
|
||||
property permissions : Hash(Acl::Path, Acl::Perm)
|
||||
property default : Acl::Perm
|
||||
|
||||
# Create a new named Group with optional parameters.
|
||||
#
|
||||
# - *name* is the name of the group (arbitrary `String`).
|
||||
# - *permissions* is a hash of ``{Acl::Path.new("path") => `Perm`}``.
|
||||
# - *default* is the value used for every path not defined in the *permissions*.
|
||||
#
|
||||
# ```
|
||||
# guest = Acl::Group.new(name: "guest", default: Acl::Perm::None, permissions: {Acl::Path.new "/public" => Acl::Perm::Read})
|
||||
# user = Acl::Group.new(name: "user", default: Acl::Perm::Read, permissions: {Acl::Path.new "/protected" => Acl::Perm::None})
|
||||
# admin = Acl::Group.new(name: "admin", default: Acl::Perm::Write)
|
||||
# ```
|
||||
def initialize(@name,
|
||||
@permissions : Hash(Acl::Path, Acl::Perm) = Hash(Acl::Path, Acl::Perm).new,
|
||||
@default : Acl::Perm = Acl::Perm::None)
|
||||
end
|
||||
|
||||
# ```
|
||||
# guest = Acl::Group.new(name: "guest", default: Acl::Perm::None, permissions: {"/public" => Acl::Perm::Read})
|
||||
# user = Acl::Group.new(name: "user", default: Acl::Perm::Read, permissions: {"/protected" => Acl::Perm::None})
|
||||
# admin = Acl::Group.new(name: "admin", default: Acl::Perm::Write)
|
||||
# ```
|
||||
def initialize(@name,
|
||||
permissions : Hash(String, Acl::Perm),
|
||||
@default : Acl::Perm = Acl::Perm::None)
|
||||
@permissions = permissions.map { |k, v| {Acl::Path.new(k), v} }.to_h
|
||||
end
|
||||
|
||||
# Check if the group as the `Acl::Perm` required to have access to a given path.
|
||||
#
|
||||
# - *path* is the path that must be checked
|
||||
# - *access* is the minimal `Acl::Perm` required for a given operation
|
||||
# ```
|
||||
# guest = Acl::Group.new(name: "guest", default: Acl::Perm::None, permissions: {"/public" => Acl::Perm::Read})
|
||||
# guest.permitted "/public", Acl::Perm::Read # => true
|
||||
# guest.permitted "/public", Acl::Perm::Write # => false
|
||||
# guest.permitted "/other", Acl::Perm::Read # => false
|
||||
# ```
|
||||
def permitted?(path : String, access : Acl::Perm) : Bool
|
||||
matched_permissions = @permissions.select { |pe, _| pe.acl_match?(path) }
|
||||
if matched_permissions.empty?
|
||||
default.to_i >= access.to_i
|
||||
else
|
||||
# keep the longuest path
|
||||
match = matched_permissions.reduce { |l, r| l[0] >= r[0] ? l : r }
|
||||
match[1].to_i >= access.to_i
|
||||
end
|
||||
end
|
||||
|
||||
# Tries to match the *path* with the permissions of this group.
|
||||
# If select every matching path and get the maximum permission among them.
|
||||
def matching?(path : String) : Acl::Perm?
|
||||
founds = @permissions.select { |ppath, pgroup| ppath.acl_match?(path) }
|
||||
return nil if founds.empty?
|
||||
found_min_size = founds.reduce { |left, right| left[0].size >= right[0].size ? left : right }
|
||||
found_min_size[1]
|
||||
end
|
||||
|
||||
# Same than Path[String]? but returns the defaut value if not found
|
||||
def matching(path : String) : Acl::Perm
|
||||
acl = self.matching?(path)
|
||||
return @default if acl.nil?
|
||||
acl
|
||||
end
|
||||
|
||||
# Tries to find the exact *path* with the permissions of this group.
|
||||
def []?(path : String) : Acl::Perm?
|
||||
found = @permissions.find { |ppath, pgroup| ppath == path }
|
||||
found && found[1]
|
||||
end
|
||||
|
||||
# Same than Path[String]? but returns the defaut value if not found
|
||||
def [](path : String) : Acl::Perm
|
||||
acl = self[path]?
|
||||
return @default if acl.nil?
|
||||
acl
|
||||
end
|
||||
|
||||
# If a path exists, replace it with the given permission *acl*, else create it.
|
||||
def []=(path : String, acl : Acl::Perm)
|
||||
replace = @permissions.find { |ppath, _| ppath == path }
|
||||
if replace
|
||||
@permissions[replace[0]] = acl
|
||||
else
|
||||
@permissions[Acl::Path.new(path)] = acl
|
||||
end
|
||||
end
|
||||
|
||||
# Remove the permissions associated to the path
|
||||
def delete(path : String)
|
||||
@permissions.reject! { |current_path| current_path.to_s == path }
|
||||
self
|
||||
end
|
||||
end
|
|
@ -1,178 +0,0 @@
|
|||
require "yaml"
|
||||
|
||||
require "./perm"
|
||||
require "./group"
|
||||
require "./entity"
|
||||
require "../lockable"
|
||||
|
||||
# The Groups is used to handle a set of uniq `Group`, by *name*.
|
||||
# It simplifies indexing, serializing, and interaction with other entities that may own several groups.
|
||||
class Acl::Groups < Lockable
|
||||
include YAML::Serializable
|
||||
|
||||
property file : String
|
||||
property groups : Hash(String, Acl::Group)
|
||||
|
||||
# The only parameter **file** is a path to synchronize the data with an harddrive
|
||||
# to keep data when stopping/restarting the wiki.
|
||||
#
|
||||
# Usage:
|
||||
# ```
|
||||
# acls = Acl::Groups.new
|
||||
# g1 = Acl::Group.new(name: "user", default: Acl::Perm::Read, permissions: {"/tmp/protected" => Acl::Perm::None})
|
||||
# g2 = Acl::Group.new(name: "admin", default: Acl::Perm::Write)
|
||||
# acls.add g1
|
||||
# acls.add g2
|
||||
# ```
|
||||
def initialize(@file)
|
||||
@groups = {} of String => Acl::Group
|
||||
end
|
||||
|
||||
# Erase the data of the file on harddrive with the current state in memory
|
||||
def save!
|
||||
File.write(@file, to_yaml)
|
||||
self
|
||||
end
|
||||
|
||||
# Read the file on harddrive and erase the current state in memory
|
||||
# NOTE: do nothing if the file does not exists
|
||||
def load!
|
||||
if File.exists?(@file) && (new_groups = Acl::Groups.read(@file) rescue nil)
|
||||
@groups = new_groups.groups
|
||||
# @file = groups.file
|
||||
else
|
||||
@groups = {} of String => Acl::Group
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# Unserialize a raw data in a file
|
||||
def self.read(file : String) : Acl::Groups
|
||||
Acl::Groups.from_yaml File.read(file)
|
||||
end
|
||||
|
||||
# Check if an `Entity` has a group with the required permissions to operate.
|
||||
#
|
||||
# ```
|
||||
# acls = Groups.new...user = User.new...acls.permitted?(user, "/my/path", Perm::Read)
|
||||
# ```
|
||||
def permitted?(entity : Acl::Entity, path : String, access : Acl::Perm)
|
||||
entity.groups.any? do |group|
|
||||
@groups[group]? ? @groups[group].permitted?(path, access) : false
|
||||
end
|
||||
end
|
||||
|
||||
# def if_permitted(entity : Acl::Entity, path : String, access : Acl::Perm)
|
||||
# yield block if permitted? entity, path, access
|
||||
# end
|
||||
|
||||
# Add a group in the current state.
|
||||
# NOTE: Overwrite conflicting existing group
|
||||
def add(group : String)
|
||||
@groups[group] = Group.new(group)
|
||||
self
|
||||
end
|
||||
|
||||
# Add a group in the current state.
|
||||
# NOTE: Overwrite conflicting existing group
|
||||
def add(group : Acl::Group)
|
||||
@groups[group.name] = group
|
||||
group
|
||||
end
|
||||
|
||||
# Remove an existing group
|
||||
# NOTE: do nothing if the group is not found
|
||||
def delete(group : String)
|
||||
@groups.delete group
|
||||
self
|
||||
end
|
||||
|
||||
# Remove an existing group
|
||||
# NOTE: do nothing if the group is not found
|
||||
def delete(group : Acl::Group)
|
||||
@groups.delete group.name
|
||||
self
|
||||
end
|
||||
|
||||
# Access an existing group
|
||||
# NOTE: raise an error if the group is not found
|
||||
def [](group : String) : Acl::Group
|
||||
@groups[group]
|
||||
end
|
||||
|
||||
# Access an existing group
|
||||
# NOTE: raise an error if the group is not found
|
||||
def [](group : Acl::Group) : Acl::Group
|
||||
@groups[group.name]
|
||||
end
|
||||
|
||||
# Access an existing group
|
||||
# NOTE: nil if not found
|
||||
def []?(group : String) : Acl::Group?
|
||||
@groups[group]?
|
||||
end
|
||||
|
||||
# Access an existing group
|
||||
# NOTE: nil if not found
|
||||
def []?(group : Acl::Group) : Acl::Group?
|
||||
@groups[group.name]?
|
||||
end
|
||||
|
||||
# Test if a group already exists in the current state
|
||||
def group_exists?(group : String) : Bool
|
||||
@groups.keys.includes? group
|
||||
end
|
||||
|
||||
# Test if a group already exists in the current state
|
||||
def group_exists?(group : Acl::Groum) : Bool
|
||||
group_exists? group.name
|
||||
end
|
||||
|
||||
# List the groups having at least the permission *acl_min* on a path
|
||||
def groups_having_direct_access_to(path : String, acl_min : Acl::Perm, not_more : Bool = false) : Array(String)
|
||||
@groups.select do |_, group|
|
||||
current_acl = (group[path]? || Acl::Perm::None).to_i
|
||||
if not_more
|
||||
current_acl == acl_min.to_i
|
||||
else
|
||||
current_acl >= acl_min.to_i
|
||||
end
|
||||
end.keys
|
||||
end
|
||||
|
||||
# Similar to `#groups_having_direct_access_to` but it only check exact path, without globbing matching
|
||||
def groups_having_any_access_to(path : String, acl_min : Acl::Perm, not_more : Bool = false) : Array(String)
|
||||
@groups.select do |_, group|
|
||||
current_acl = (group.matching?(path) || Acl::Perm::None).to_i
|
||||
if not_more
|
||||
current_acl == acl_min.to_i
|
||||
else
|
||||
current_acl >= acl_min.to_i
|
||||
end
|
||||
end.keys
|
||||
end
|
||||
|
||||
# The quickest way possible to give a permission to a list of groups to a resource
|
||||
def add_permissions_to(path : String, groups : Array(String), acl : Acl::Perm)
|
||||
groups.each do |group|
|
||||
self.add group unless group_exists? group
|
||||
old_acl = self[group][path]?
|
||||
self[group][path] = acl if old_acl.nil? || old_acl.to_i < acl.to_i
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# Remove all positive permissions on a path for every existin group
|
||||
def clear_permissions_of(path : String)
|
||||
self.clear_permissions_of(path, Acl::Perm::Read)
|
||||
self.clear_permissions_of(path, Acl::Perm::Write)
|
||||
end
|
||||
|
||||
# Remove a given permission on a path for every existin group
|
||||
def clear_permission_of(path : String, acl : Acl::Perm)
|
||||
@groups.each do |_, group|
|
||||
group.delete(path) if group[path]? == acl
|
||||
end
|
||||
self
|
||||
end
|
||||
end
|
|
@ -1,44 +0,0 @@
|
|||
require "yaml"
|
||||
|
||||
# An Acl Path identify one or several resources based on joker `*`
|
||||
# in order to match several files.
|
||||
class Acl::Path
|
||||
include YAML::Serializable
|
||||
property value : String
|
||||
|
||||
@[YAML::Field(key: "regex", ignore: true)]
|
||||
getter regex : Regex?
|
||||
|
||||
def self.value_to_regex(value : String)
|
||||
value_regex = value.gsub "*", ".*"
|
||||
Regex.new "^#{value_regex}$"
|
||||
end
|
||||
|
||||
def initialize(@value : String)
|
||||
@regex = Acl::Path.value_to_regex @value
|
||||
end
|
||||
|
||||
def acl_match?(other_path : String) : Bool
|
||||
@regex ||= Acl::Path.value_to_regex @value
|
||||
!!@regex.as(Regex).match other_path
|
||||
end
|
||||
|
||||
def to_s
|
||||
@value
|
||||
end
|
||||
|
||||
def size
|
||||
# +3 = ".md".size
|
||||
@value.includes?("*") ? @value.size : @value.size + 3
|
||||
end
|
||||
|
||||
def ==(rhs)
|
||||
self.to_s == rhs.to_s
|
||||
end
|
||||
|
||||
{% for op in [">", "<", ">=", "<="] %}
|
||||
def {{op.id}}(rhs : Path)
|
||||
self.size {{op.id}} rhs.size
|
||||
end
|
||||
{% end %}
|
||||
end
|
|
@ -1,14 +0,0 @@
|
|||
# Permission levels of the Acl system.
|
||||
#
|
||||
# The values are ordered and hide a bitmask (read=1, write=2, read+write=3) but for simplicity at usage,
|
||||
# since nothing can have the value 2, we don't explicitly have a write-only value at 2.
|
||||
enum Acl::Perm
|
||||
# level 0. Cannot read, cannot write.
|
||||
None = 0
|
||||
|
||||
# level 1. Can read, cannot write.
|
||||
Read = 1
|
||||
|
||||
# level 3. Can read, can write.
|
||||
Write = 3
|
||||
end
|
|
@ -1,51 +0,0 @@
|
|||
# A `FileTree` is a tree structure representing a file with a name and subfiles.
|
||||
# It is used to map the wiki for the "sitemap" feature.
|
||||
# TODO: it should be fully replaced by the index
|
||||
class Wikicr::FileTree
|
||||
getter name : String
|
||||
getter files : Array(FileTree)
|
||||
|
||||
# Build a FileTree that represents the real structure of the "to_scan"
|
||||
# It is recursive and may be very time consuming, so there is a limit of depth
|
||||
#
|
||||
# ```
|
||||
# FileTree.build("./data/")
|
||||
# ```
|
||||
def self.build(to_scan : String, max_depth : Int = 32) : FileTree
|
||||
# Stop the recursion
|
||||
return FileTree.new to_scan if max_depth < 1
|
||||
|
||||
# Save the current directory before getting in
|
||||
dir_current = Dir.current
|
||||
Dir.cd to_scan
|
||||
|
||||
# List the files, and filter them
|
||||
all_files = Dir.entries "."
|
||||
all_files.select! { |file| !(file =~ /^\./) }
|
||||
all_files.sort!
|
||||
|
||||
# Separate files and directory
|
||||
# For the directories, call this function recursively
|
||||
files = all_files
|
||||
.select { |file| !File.directory? file }
|
||||
.map { |file| FileTree.new(file).as(FileTree) }
|
||||
directories = all_files
|
||||
.select { |file| File.directory? file }
|
||||
.map { |file| FileTree.new(file, FileTree.build(file, max_depth - 1).files).as(FileTree) }
|
||||
|
||||
# Generate the file with the list of the files and the directories
|
||||
structure = FileTree.new to_scan, files + directories
|
||||
|
||||
# Get out of the parent and return the current directory object
|
||||
Dir.cd dir_current
|
||||
structure
|
||||
end
|
||||
|
||||
def initialize(@name, @files = [] of FileTree)
|
||||
end
|
||||
|
||||
# If the file contains other files, then it is a "directory"
|
||||
def directory? : Bool
|
||||
!@files.empty?
|
||||
end
|
||||
end
|
|
@ -1,30 +1,17 @@
|
|||
require "./options"
|
||||
|
||||
module Wikicr::Git
|
||||
module Wikicr
|
||||
REPO = Git::Repo.open(Wikicr::OPTIONS.basedir)
|
||||
|
||||
extend self
|
||||
|
||||
# Initialize the data repository (where the pages are stored).
|
||||
def init!
|
||||
Dir.mkdir_p Wikicr::OPTIONS.basedir
|
||||
current = Dir.current
|
||||
Dir.cd Wikicr::OPTIONS.basedir
|
||||
`git init .`
|
||||
Dir.cd current
|
||||
def repo
|
||||
REPO
|
||||
end
|
||||
|
||||
# Save the modifications on the *file* into the git repository
|
||||
# TODO: lock before commit
|
||||
# TODO: security of jailed_file and self.name ?
|
||||
def commit!(user : Wikicr::User, message, files : Array(String) = [] of String)
|
||||
dir = Dir.current
|
||||
begin
|
||||
Dir.cd Wikicr::OPTIONS.basedir
|
||||
puts `git add -- #{files.join(" ")}`
|
||||
puts `git commit --no-gpg-sign --author "#{user.name} <#{user.name}@localhost>" -m "#{message}" -- #{files.join(" ")}`
|
||||
ensure
|
||||
Dir.cd dir
|
||||
end
|
||||
def init!
|
||||
end
|
||||
end
|
||||
|
||||
Wikicr::Git.init!
|
||||
require "./users"
|
||||
Wikicr::Page.new("testX").write("OK", Wikicr::USERS.read!.find("arthur"))
|
||||
|
|
15
src/lib/index.cr
Normal file
15
src/lib/index.cr
Normal file
|
@ -0,0 +1,15 @@
|
|||
require "./page"
|
||||
require "./sfile"
|
||||
|
||||
# Index handler, to build and parse index files that keep a list of files and meta-data
|
||||
class Wikicr::Page::Index
|
||||
# getter dir : String
|
||||
# getter pages : Array(SFile)
|
||||
|
||||
# def initialize(@dir)
|
||||
# @pages = Dir.entries(@dir).map { |f| SFile.build(f) }
|
||||
# end
|
||||
|
||||
# def build!
|
||||
# end
|
||||
end
|
|
@ -1,28 +0,0 @@
|
|||
# Lockable is an abstract class that provides a function `#transaction!` that
|
||||
# allows the class to execute some code that requires to do not conflict with
|
||||
# other operations. It is usually linked with an IO (a file).
|
||||
abstract class Lockable
|
||||
abstract def load!
|
||||
abstract def save!
|
||||
|
||||
@[YAML::Field(key: "lock", ignore: true)]
|
||||
@lock : Mutex = Mutex.new
|
||||
|
||||
# Execute some operation on the object, and then save it.
|
||||
# The content can be loaded before executing the operations optionnaly.
|
||||
#
|
||||
# ```
|
||||
# someLockableObject.transaction(read: true) { |obj| obj.update_operation(...) }
|
||||
# ```
|
||||
def transaction!(read = false)
|
||||
@lock.synchronize do
|
||||
begin
|
||||
self.load! if read == true
|
||||
yield self
|
||||
ensure
|
||||
self.save!
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,12 +1,12 @@
|
|||
module Wikicr
|
||||
class Options
|
||||
getter basedir : String
|
||||
class Wikicr::Options
|
||||
getter basedir : String
|
||||
|
||||
def initialize
|
||||
@basedir = File.expand_path ENV.fetch("WIKI_DATA", "data"), Dir.current
|
||||
Dir.mkdir_p @basedir rescue nil
|
||||
end
|
||||
def initialize
|
||||
@basedir = File.expand_path(ENV["WIKI_DATA"]? || "data", Dir.current)
|
||||
Dir.mkdir_p(@basedir) rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
module Wikicr
|
||||
OPTIONS = Wikicr::Options.new
|
||||
end
|
||||
|
|
199
src/lib/page.cr
199
src/lib/page.cr
|
@ -1,147 +1,108 @@
|
|||
require "uri"
|
||||
|
||||
require "./errors"
|
||||
require "./page/*"
|
||||
require "./sfile"
|
||||
|
||||
# A `Page` is the representation in the wiki of something that can be accessed
|
||||
# from an url /pages/*path.
|
||||
#
|
||||
# It is used to associate path, url and data.
|
||||
# Is is can also jails the path into the *OPTIONS.basedir* to be sure that
|
||||
# there is no attack by writing files outside of the directory where the pages
|
||||
# must be stored.
|
||||
class Wikicr::Page
|
||||
include Wikicr::Page::TableOfContentReader
|
||||
include Wikicr::Page::TagsReader
|
||||
# A Page is a file and an url part
|
||||
# Is is used to jail files into the OPTIONS.basedir
|
||||
struct Wikicr::Page
|
||||
getter file : String
|
||||
getter name : String
|
||||
|
||||
# Directory where the pages are stored
|
||||
PAGES_SUB_DIRECTORY = "pages/"
|
||||
|
||||
# Beginning of the url of a page
|
||||
URL_PREFIX = "/pages"
|
||||
|
||||
# Path of the file that contains the page
|
||||
getter path : String
|
||||
|
||||
# Url of the page (without any prefix)
|
||||
getter url : String
|
||||
|
||||
# Complete Url of the page
|
||||
getter real_url : String
|
||||
|
||||
# Title of the page
|
||||
getter title : String
|
||||
|
||||
# A way to hold the tags without having to compute it again and again
|
||||
# you need to fill it yourself (with the wikimd patch)
|
||||
property tags : Array(String)
|
||||
|
||||
def initialize(url : String, real_url : Bool = false, parse_title : Bool = false)
|
||||
@tags = [] of String
|
||||
url = Page.sanitize(url)
|
||||
if real_url
|
||||
@real_url = url
|
||||
@url = @real_url[URL_PREFIX.size..-1].strip "/"
|
||||
else
|
||||
@url = url.strip "/"
|
||||
@real_url = File.expand_path @url, URL_PREFIX
|
||||
end
|
||||
@path = Page.url_to_file @url
|
||||
@title = File.basename @url
|
||||
@title = Page.parse_title(@path) || @title if parse_title && File.exists? @path
|
||||
end
|
||||
|
||||
def self.parse_title(path : String) : String?
|
||||
title = File.read(path).split("\n").find { |l| l.starts_with? "# " }
|
||||
title && title.strip("# ").strip
|
||||
end
|
||||
|
||||
# basic treatment of the url
|
||||
# it is NOT a security measure
|
||||
def self.sanitize(url : String)
|
||||
Index::Entry.title_to_slug url
|
||||
end
|
||||
|
||||
def parse_title!
|
||||
@title = Page.parse_title(@path) || @title if File.exists?(@path)
|
||||
def initialize(@name)
|
||||
@file = Page.name_to_file(@name)
|
||||
end
|
||||
|
||||
# translate a name ("/test/title" for example)
|
||||
# into a file path ("/srv/data/test/ttle.md)
|
||||
def self.url_to_file(url : String)
|
||||
page_dir = File.expand_path Wikicr::OPTIONS.basedir, PAGES_SUB_DIRECTORY
|
||||
page_file = File.expand_path Page.sanitize(url), page_dir
|
||||
page_file + ".md"
|
||||
def self.name_to_file(name : String)
|
||||
File.expand_path(name + ".md", Wikicr::OPTIONS.basedir)
|
||||
end
|
||||
|
||||
# verify if the *file* is in the current dir (avoid ../ etc.)
|
||||
# it will raise a `Error403` if the file is out of the basedir
|
||||
def jail
|
||||
# :unused:
|
||||
# # translate a file into a name
|
||||
# # @see #name_to_file
|
||||
# def self.file_to_name(file : String)
|
||||
# file.chomp(".md")[Wikicr::OPTIONS.basedir.size..-1]
|
||||
# end
|
||||
|
||||
# :unused:
|
||||
# # set a new file name, an update the file path
|
||||
# def name=(name)
|
||||
# @name = name
|
||||
# @file = Page.name_to_file @name
|
||||
# end
|
||||
|
||||
# :unused:
|
||||
# # set a new file path, and update the file name
|
||||
# def file=(file)
|
||||
# @file = File.expand_path file
|
||||
# @name = Page.file_to_name @file
|
||||
# end
|
||||
|
||||
# verify if the file is in the current dir (avoid ../ etc.)
|
||||
def jail(user : User)
|
||||
chroot = Wikicr::OPTIONS.basedir
|
||||
# TODO: consider security of ".git/"
|
||||
# TODO: read ACL for user
|
||||
|
||||
# the @file is already expanded (File.expand_path) in the constructor
|
||||
if Wikicr::OPTIONS.basedir != @path[0..(Wikicr::OPTIONS.basedir.size - 1)]
|
||||
raise Error403.new "Out of chroot (#{@path} on #{Wikicr::OPTIONS.basedir})"
|
||||
if chroot != @file[0..(chroot.size - 1)]
|
||||
raise Error403.new "Out of chroot (#{@file} on #{chroot})"
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# Get the directory of the *file* (~/data/test/home becomes ~/data/test)
|
||||
def dirname
|
||||
File.dirname @path
|
||||
File.dirname self.file
|
||||
end
|
||||
|
||||
# Url without the page itself (/pages/test/home becomes /test)
|
||||
def url_dirname
|
||||
File.dirname @url
|
||||
def read(user : User)
|
||||
self.jail user
|
||||
File.read self.file
|
||||
end
|
||||
|
||||
# Real url without the page itself (/pages/test/home becomes /pages/test)
|
||||
def real_url_dirname
|
||||
File.dirname @real_url
|
||||
end
|
||||
|
||||
# Reads the *file* and returns the content.
|
||||
def read
|
||||
self.jail
|
||||
File.read @path
|
||||
end
|
||||
|
||||
# TODO: verify if the new_page already exists
|
||||
# Move the current page into another place and commit
|
||||
def rename(user : Wikicr::User, new_url) : Page
|
||||
self.jail
|
||||
new_page = Wikicr::Page.new new_url
|
||||
new_page.jail
|
||||
Dir.mkdir_p File.dirname new_page.path
|
||||
File.rename self.path, new_page.path
|
||||
Wikicr::Git.commit! user, message: "rename #{@url}", files: [@path, new_page.path]
|
||||
new_page
|
||||
end
|
||||
|
||||
# Writes into the *file*, and commit.
|
||||
def write(user : Wikicr::User, body)
|
||||
self.jail
|
||||
def write(body, user : User)
|
||||
self.jail user
|
||||
Dir.mkdir_p self.dirname
|
||||
is_new = File.exists? @path
|
||||
File.write @path, body
|
||||
Wikicr::Git.commit! user, message: (is_new ? "create #{@url}" : "update #{@url}"), files: [@path]
|
||||
File.write self.file, body
|
||||
commit!(user)
|
||||
end
|
||||
|
||||
# Deletes the *file*, and commit
|
||||
def delete(user : Wikicr::User)
|
||||
self.jail
|
||||
File.delete @path
|
||||
Wikicr::Git.commit! user, message: "delete #{@url}", files: [@path]
|
||||
private def commit!(user)
|
||||
puts "--------------- COMMIT ! ------------"
|
||||
# You can check git_repository_head_unborn() to see if HEAD points at a reference or not.
|
||||
tree_id = Pointer(Git::C::Oid).null
|
||||
parent_id = Pointer(Git::C::Oid).null
|
||||
commit_id = Pointer(Git::C::Oid).null
|
||||
tree = nil.as(Git::C::X_Tree)
|
||||
parent = nil.as(Git::C::X_Commit)
|
||||
index = nil.as(Git::C::X_Index)
|
||||
puts "repository_index"
|
||||
puts Git::C.repository_index(pointerof(index), Wikicr.repo.safe)
|
||||
pp index, index.address, index.value
|
||||
puts "index_write_tree"
|
||||
puts Git::C.index_write_tree(tree.as(Pointer(Git::C::Oid)), index)
|
||||
pp tree
|
||||
|
||||
puts "reference_name_to_id"
|
||||
puts Git::C.reference_name_to_id(parent_id, Wikicr.repo.safe, "HEAD")
|
||||
puts "commit_lookup"
|
||||
puts Git::C.commit_lookup(pointerof(parent), Wikicr.repo.safe, parent_id)
|
||||
|
||||
sign = Pointer(Git::C::Signature).null
|
||||
puts "signature_now"
|
||||
puts Git::C.signature_now(pointerof(sign), user.name, "#{user.name}@localhost")
|
||||
|
||||
puts "commit_create"
|
||||
puts Git::C.commit_create(commit_id, Wikicr.repo.safe, "HEAD", sign, sign, "UTF-8", "update #{self.name}", tree.value, 1, pointerof(parent))
|
||||
end
|
||||
|
||||
# Checks if the *file* exists
|
||||
def exists?
|
||||
self.jail
|
||||
File.exists? @path
|
||||
def delete(user : User)
|
||||
self.jail user
|
||||
File.delete self.file
|
||||
end
|
||||
|
||||
def exists?(user : User)
|
||||
self.jail user
|
||||
File.exists? self.file
|
||||
end
|
||||
end
|
||||
|
||||
# require "./users"
|
||||
# require "./git"
|
||||
# Wikicr::Page.new("testX").write("OK", Wikicr::USERS.load!.find("arthur.poulet@mailoo.org"))
|
||||
|
|
|
@ -1,158 +0,0 @@
|
|||
require "yaml"
|
||||
|
||||
require "./index/entry"
|
||||
|
||||
# And Index is an object that associate a file with a lot of meta-data
|
||||
# like related url, the title, the table of content, ...
|
||||
class Wikicr::Page
|
||||
class Index < Lockable
|
||||
alias Entries = Hash(String, Entry) # path, entry
|
||||
|
||||
include YAML::Serializable
|
||||
property file : String
|
||||
property entries : Entries
|
||||
|
||||
# property find_index : Hash(String, String)
|
||||
# property find_tags : Hash(String, Array(String))
|
||||
|
||||
def initialize(@file : String)
|
||||
@entries = Entries.new
|
||||
# @find_index = of String => String
|
||||
# @find_tags = of String => Array(String)
|
||||
end
|
||||
|
||||
# Find a matching *text* into the Index.
|
||||
# If no matching content, return a default value.
|
||||
def one_by_title_or_url(text : String, context : Page, raise_not_found : Bool = false) : Index::Entry
|
||||
found = find_by_title(text, context) || find_by_url(text, context)
|
||||
if found.nil?
|
||||
raise "Page not found" if raise_not_found
|
||||
STDERR.puts "warning: no page \"#{text}\" found"
|
||||
Index::Entry.from_context(context, text)
|
||||
else
|
||||
return found
|
||||
end
|
||||
end
|
||||
|
||||
# Find a specific url into the Index.
|
||||
# If no matching content, return a default value.
|
||||
# If the page is not found, the title of the entry will be the default_title or the url
|
||||
def one_by_url(url : String, context : Page, default_title : String? = nil, raise_not_found : Bool = false) : Index::Entry
|
||||
found = find_by_url(url, context)
|
||||
if found.nil?
|
||||
raise "Page not found" if raise_not_found
|
||||
STDERR.puts "warning: no page \"#{url}\" found"
|
||||
title = default_title || url
|
||||
Index::Entry.from_context(context, title, url)
|
||||
else
|
||||
return found
|
||||
end
|
||||
end
|
||||
|
||||
TAG_SIGN_REQUIRE = '+'
|
||||
TAG_SIGN_FORBIDE = '-'
|
||||
TAG_SIGNS = {
|
||||
TAG_SIGN_REQUIRE,
|
||||
TAG_SIGN_FORBIDE,
|
||||
}
|
||||
|
||||
# Find a matching *text* into the Index.
|
||||
# If no matching content, return a default value.
|
||||
# @param tag_line must be a "tag +andtagx -butnottagy ortag3"
|
||||
def all_by_tags(tags_line : String, context : Page) : Entries
|
||||
tags = tags_line.split(' ')
|
||||
required_tags = tags.select { |tag| tag[0] == TAG_SIGN_REQUIRE }.map { |tag| tag[1..-1] }
|
||||
forbidden_tags = tags.select { |tag| tag[0] == TAG_SIGN_FORBIDE }.map { |tag| tag[1..-1] }
|
||||
at_least_one_tags = tags.select { |tag| !TAG_SIGNS.includes? tag[0] }
|
||||
@entries.select do |url, entry|
|
||||
(entry.tags & required_tags).size == required_tags.size &&
|
||||
(entry.tags & forbidden_tags).size == 0 &&
|
||||
(entry.tags & at_least_one_tags).size > 0
|
||||
end
|
||||
end
|
||||
|
||||
# Find the closest `Index`' `Entry` to *text* based on the entries title
|
||||
# and searching for the closer url as possible to the context
|
||||
private def find_by_title(text : String, context : Page) : Entry?
|
||||
# exact_matched = @entries.select{|_, entry| entry.title == text }.values
|
||||
# return choose_closer_url(exact_matched, context) unless exact_matched.empty?
|
||||
slug_matched = @entries.select { |_, entry|
|
||||
entry.slug == Index::Entry.title_to_slug(text)
|
||||
}.values
|
||||
return choose_closer_url(slug_matched, context) unless slug_matched.empty?
|
||||
nil
|
||||
end
|
||||
|
||||
# Find the url which is the closest as possible than the context url (start with the maxmimum common chars).
|
||||
private def choose_closer_url(entries : Array(Entry), context : Page) : Entry
|
||||
raise "Cannot handle empty array" if entries.empty?
|
||||
entries.reduce { |lhs, rhs| Index.url_closeness(context.url, lhs.url) >= Index.url_closeness(context.url, rhs.url) ? lhs : rhs }
|
||||
end
|
||||
|
||||
# Computes the amount of common chars at the beginning of each string
|
||||
def self.url_closeness(from : String, to : String)
|
||||
from.size.times do |i|
|
||||
return i if from[i] != to[i]
|
||||
end
|
||||
return from.size
|
||||
end
|
||||
|
||||
private def find_by_url(text : String, context : Page) : Entry?
|
||||
slug_matched = @entries.select { |_, entry|
|
||||
entry.url == Index::Entry.title_to_slug(text) ||
|
||||
entry.url == File.join(context.url_dirname, Index::Entry.title_to_slug(text))
|
||||
}.values
|
||||
return choose_closer_url(slug_matched, context) unless slug_matched.empty?
|
||||
nil
|
||||
end
|
||||
|
||||
# Access to an existing `Entry`.
|
||||
def [](page : Wikicr::Page) : Index::Entry
|
||||
@entries[page.path]
|
||||
end
|
||||
|
||||
# Access to an existing `Entry`.
|
||||
def []?(page : Wikicr::Page) : Index::Entry?
|
||||
@entries[page.path]?
|
||||
end
|
||||
|
||||
# Add a new `Entry`.
|
||||
def add(page : Wikicr::Page)
|
||||
@entries[page.path] = Entry.new(
|
||||
path: page.path,
|
||||
url: page.url,
|
||||
title: page.title,
|
||||
read_toc: true,
|
||||
tags: page.tags,
|
||||
)
|
||||
self
|
||||
end
|
||||
|
||||
# Remove an `Entry` from the `Index` based on its path.
|
||||
def delete(page : Wikicr::Page)
|
||||
@entries.delete page.path
|
||||
self
|
||||
end
|
||||
|
||||
# Replace the old Index using the state registrated into the *file*.
|
||||
def load!
|
||||
if File.exists?(@file) && (new_index = Index.read(@file) rescue nil)
|
||||
@entries = new_index.entries
|
||||
# @file = index.file
|
||||
else
|
||||
@entries = {} of String => Entry
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
def self.read(file : String)
|
||||
Index.from_yaml File.read(file)
|
||||
end
|
||||
|
||||
# Save the current state into the file
|
||||
def save!
|
||||
File.write @file, self.to_yaml
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,40 +0,0 @@
|
|||
require "../table_of_content"
|
||||
require "../tags"
|
||||
require "../../lockable"
|
||||
|
||||
# And Index is an object that associate a file with a lot of meta-data
|
||||
# like related url, the title, the table of content, ...
|
||||
class Wikicr::Page
|
||||
class Index < Lockable
|
||||
class Entry
|
||||
alias Tag = String
|
||||
alias Tags = Array(Tag)
|
||||
|
||||
include YAML::Serializable
|
||||
property path : String # path of the file /srv/wiki/data/xxx
|
||||
property url : String # real url of the page /pages/xxx
|
||||
property title : String # Any title
|
||||
property slug : String # Exact matching title
|
||||
property toc : Page::TableOfContentReader::Toc
|
||||
property tags : Tags
|
||||
|
||||
def initialize(@path, @url, @title, @tags : Tags = Tags.new, read_toc : Bool = false)
|
||||
@slug = Entry.title_to_slug title
|
||||
@toc = read_toc ? Page::TableOfContentReader.toc(@path) : Page::TableOfContentReader::Toc.new
|
||||
end
|
||||
|
||||
def self.from_context(context : Page, title : String, url : String? = nil)
|
||||
url = url || title
|
||||
new(
|
||||
title: title,
|
||||
url: File.join(context.real_url_dirname, Entry.title_to_slug(url)),
|
||||
path: File.join(context.dirname, "#{Entry.title_to_slug(url)}.md"),
|
||||
)
|
||||
end
|
||||
|
||||
def self.title_to_slug(title : String) : String
|
||||
title.gsub(/[\s\.]/, '-').gsub(/-+/, '-').downcase
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,32 +0,0 @@
|
|||
class Wikicr::Page
|
||||
module TableOfContentReader
|
||||
alias TocLine = {Int32, String}
|
||||
alias Toc = Array(TocLine)
|
||||
|
||||
# The table of content of the file
|
||||
def toc : Toc
|
||||
TableOfContentReader.toc @path
|
||||
end
|
||||
|
||||
def self.toc(path : String) : Toc
|
||||
toc = Toc.new
|
||||
File.open path, "r" do |f|
|
||||
while line = f.gets
|
||||
toc_line = get_toc_line line
|
||||
toc << toc_line.as(TocLine) unless toc_line.nil?
|
||||
end
|
||||
end
|
||||
pp toc
|
||||
toc
|
||||
end
|
||||
|
||||
# Parse a markdown line, and return a TocLine if it is a title
|
||||
def self.get_toc_line(line : String) : TocLine?
|
||||
if match = line.match /^(\#{1,6})\s(.+)/
|
||||
title_num = match[1].size
|
||||
title = match[2]
|
||||
{title_num, title}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,11 +0,0 @@
|
|||
class Wikicr::Page
|
||||
module TagsReader
|
||||
def parse_tags!(index : Index)
|
||||
Wikicr::MarkdPatch.to_html(input: self.read, index: index, context: self, parse_tags: true)
|
||||
end
|
||||
|
||||
# def self.all(path : String) : Index::Entry::Tags
|
||||
# Index::Entry::Tags.new
|
||||
# end
|
||||
end
|
||||
end
|
26
src/lib/session-init.cr
Normal file
26
src/lib/session-init.cr
Normal file
|
@ -0,0 +1,26 @@
|
|||
Session.config do |config|
|
||||
config.cookie_name = "session_id"
|
||||
config.secret = ENV["WIKI_SECRET"]
|
||||
config.gc_interval = 2.minutes # 2 minutes
|
||||
end
|
||||
|
||||
macro user_must_be_logged!(env)
|
||||
if user_signed_in?(env)
|
||||
puts "You are authenticated"
|
||||
# continue
|
||||
else
|
||||
puts "You are not authenticated"
|
||||
env.redirect "/users/login"
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
macro user_signed_in?(env)
|
||||
env.session.string?("username")
|
||||
end
|
||||
|
||||
macro current_user(env)
|
||||
%name = user_signed_in?(env)
|
||||
raise "User not signed in" if %name.nil?
|
||||
Wikicr::USERS.find(%name)
|
||||
end
|
36
src/lib/sfile.cr
Normal file
36
src/lib/sfile.cr
Normal file
|
@ -0,0 +1,36 @@
|
|||
# a SFile is a file structure with a name and subfiles
|
||||
class Wikicr::SFile
|
||||
getter name : String
|
||||
getter files : Array(SFile)
|
||||
|
||||
# Build a SFile that represents the real structure of the "to_scan"
|
||||
# It is recursive and may be very time consuming, so there is a limit of depth
|
||||
def self.build(to_scan : String, max_depth : Int = 32) : SFile
|
||||
return SFile.new(to_scan) if max_depth < 1
|
||||
dir_current = Dir.current
|
||||
Dir.cd to_scan
|
||||
|
||||
all_files = Dir.entries(".")
|
||||
all_files.select! { |file| !(file =~ /^\./) }
|
||||
|
||||
files = all_files
|
||||
.select { |file| !File.directory? file }
|
||||
.map { |file| SFile.new(file).as(SFile) }
|
||||
directories = all_files
|
||||
.select { |file| File.directory? file }
|
||||
.map { |file| SFile.new(file, SFile.build(file, max_depth - 1).files).as(SFile) }
|
||||
|
||||
structure = SFile.new(to_scan, files + directories)
|
||||
|
||||
Dir.cd dir_current
|
||||
structure
|
||||
end
|
||||
|
||||
def initialize(@name, @files = [] of SFile)
|
||||
end
|
||||
|
||||
# if the file contains other files, then it is a "directory"
|
||||
def directory? : Bool
|
||||
!@files.empty?
|
||||
end
|
||||
end
|
43
src/lib/user.cr
Normal file
43
src/lib/user.cr
Normal file
|
@ -0,0 +1,43 @@
|
|||
require "crypto/bcrypt/password"
|
||||
|
||||
class Wikicr::User
|
||||
class Invalid < Exception
|
||||
end
|
||||
|
||||
SEP = ':'
|
||||
|
||||
getter name : String
|
||||
getter password : String
|
||||
getter groups : Array(String)
|
||||
|
||||
def initialize(@name, @password, @groups = [] of String)
|
||||
end
|
||||
|
||||
def initialize(line : String)
|
||||
split = line.split SEP
|
||||
raise Invalid.new("Cannot parse this line (split.size = #{split.size}, should be 3)") if split.size != 3
|
||||
@name = split[0]
|
||||
@password = split[1]
|
||||
@groups = split[2].split(",")
|
||||
end
|
||||
|
||||
def encrypt!
|
||||
@password = Crypto::Bcrypt::Password.create(@password).to_s
|
||||
self
|
||||
end
|
||||
|
||||
def password_encrypted
|
||||
Crypto::Bcrypt::Password.new(@password)
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#{name}#{SEP}#{password}#{SEP}#{groups.join(",")}"
|
||||
end
|
||||
|
||||
def to_s(io : IO)
|
||||
io << name << SEP
|
||||
io << password << SEP
|
||||
groups.each { |g| io << g; io << ',' if g != groups.last }
|
||||
io << '\n'
|
||||
end
|
||||
end
|
114
src/lib/users.cr
Normal file
114
src/lib/users.cr
Normal file
|
@ -0,0 +1,114 @@
|
|||
require "./user"
|
||||
|
||||
# Handle an user list and file associated
|
||||
# TODO: mutex on add/remove/update
|
||||
class Wikicr::Users
|
||||
class AlreadyExist < Exception
|
||||
end
|
||||
|
||||
class NotExist < Exception
|
||||
end
|
||||
|
||||
getter file : String
|
||||
@list : Hash(String, User)
|
||||
|
||||
def initialize(@file)
|
||||
@list = {} of String => User
|
||||
# TODO: set UNIX permissions
|
||||
File.touch(@file)
|
||||
end
|
||||
|
||||
# read the users from the file (erase the modifications !)
|
||||
def read!
|
||||
@list = File.read(@file).split("\n")
|
||||
.select { |line| !line.empty? }
|
||||
.map { |line| u = User.new(line); {u.name, u} }.to_h
|
||||
self
|
||||
end
|
||||
|
||||
# save the users into the file
|
||||
def save!
|
||||
File.open(@file, "w") do |fd|
|
||||
@list.each { |name, user| user.to_s(fd) }
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# add an user to the list
|
||||
def add(u : User)
|
||||
raise AlreadyExist.new "User #{u.name} already exists" if (@list[u.name]?)
|
||||
@list[u.name] = u
|
||||
self
|
||||
end
|
||||
|
||||
# remove an user from the list
|
||||
# @see .remove(String)
|
||||
def remove(u : User)
|
||||
remove u.name
|
||||
end
|
||||
|
||||
# remove an user from the list
|
||||
def remove(name : String)
|
||||
raise NotExist.new "User #{name} is not in the list" if (!@list[name]?)
|
||||
@list.remove(name)
|
||||
self
|
||||
end
|
||||
|
||||
# replace an entry
|
||||
def update(name : String, u : User)
|
||||
raise NotExist.new "User #{name} is not in the list" if (!@list[name]?)
|
||||
|
||||
# if the name change
|
||||
if name != u.name
|
||||
add u # if it fails, remove will fail too
|
||||
remove name
|
||||
else
|
||||
@list[u.name] = u
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# find an user based on its name
|
||||
def find(name : String) : User
|
||||
raise NotExist.new "User #{name} is not in the list" if (!@list[name]?)
|
||||
@list[name]
|
||||
end
|
||||
|
||||
##################
|
||||
# HIGH LEVEL API #
|
||||
##################
|
||||
|
||||
# find an user by its name and check the password
|
||||
def auth?(name : String, password : String) : User?
|
||||
user = find(name)
|
||||
user.password_encrypted == password ? user : nil
|
||||
end
|
||||
|
||||
def auth!(name : String, password : String) : User?
|
||||
self.read!
|
||||
auth?(name, password)
|
||||
end
|
||||
|
||||
def register!(name : String, password : String, groups : Array(String) = %w(user))
|
||||
user = User.new(name, password, groups).encrypt!
|
||||
self.read!
|
||||
self.add(user)
|
||||
self.save!
|
||||
user
|
||||
end
|
||||
end
|
||||
|
||||
# file = "/tmp/users"
|
||||
# File.touch(file)
|
||||
# include Wikicr
|
||||
# users = Users.new(file)
|
||||
# users.read!
|
||||
# pp users
|
||||
# user = User.new("arthur", "passwd", %w(admin,user)).encrypt
|
||||
# users.add user
|
||||
# users.save!
|
||||
# p users
|
||||
# pp Crypto::Bcrypt::Password.new(user.password) == "passwd"
|
||||
# pp users.auth?("arthur", "passwd")
|
||||
# pp users.auth?("arthur", "passwdx")
|
||||
# pp users.auth?("arthurx", "passwd") # raise here
|
|
@ -1,55 +0,0 @@
|
|||
require "crypto/bcrypt/password"
|
||||
require "../acl/entity"
|
||||
|
||||
# An `User` is a couple name/password/groups/token.
|
||||
# The *name* and *groups* is public, the *password* and the *token* are private.
|
||||
# The password is used to recognize an user when login in with a form for example.
|
||||
# The token is used to recognize an user by a cookie for exemple.
|
||||
class Wikicr::User
|
||||
class Invalid < Exception
|
||||
end
|
||||
|
||||
include YAML::Serializable
|
||||
property name : String
|
||||
property password : String
|
||||
property groups : Array(String)
|
||||
property token : String?
|
||||
|
||||
# ```
|
||||
# User.new "admin", "password", %w(admin user)
|
||||
# User.new "nephos", "password", %w(user guest)
|
||||
# ```
|
||||
def initialize(@name, @password, @groups = [] of String, @token : String? = nil)
|
||||
raise "Invalid name #{@name}" if !@name =~ /^[A-Za-z0-9 _.-]+$/ # Security: Avoid escaping and injection of code
|
||||
end
|
||||
|
||||
# Encrypts the passwod using `Crypto::Bcrypt`.
|
||||
#
|
||||
# ```
|
||||
# Wikicr::User.new("admin", "password", %w(admin)).encrypt!.password # => "$2a$11$G2i2.Km1DRbJtqDBFRhKXuSn8IwNVt7AypAP328T1OYq0wBugkgCm"
|
||||
# ```
|
||||
def encrypt!
|
||||
@password = Crypto::Bcrypt::Password.create(@password).to_s
|
||||
self
|
||||
end
|
||||
|
||||
# Reads the password using `Crypto::Bcrypt`
|
||||
def password_encrypted
|
||||
Crypto::Bcrypt::Password.new @password
|
||||
end
|
||||
|
||||
def generate_new_token!
|
||||
@token = Random::Secure.base64 64
|
||||
end
|
||||
|
||||
#########################
|
||||
# Implement Acl::Entity #
|
||||
#########################
|
||||
include Acl::Entity
|
||||
|
||||
# getter groups : Array(String)
|
||||
|
||||
def has_group?(group : String) : Bool
|
||||
@groups.includes? group
|
||||
end
|
||||
end
|
|
@ -1,171 +0,0 @@
|
|||
require "./user"
|
||||
|
||||
# The class `Users` handles a list of `User`, with add, remove, update an find operation.
|
||||
# An instance of `Users` must be linked with a file which can be read of updated
|
||||
#
|
||||
# TODO: mutex on add/remove/update
|
||||
class Wikicr::Users < Lockable
|
||||
class AlreadyExist < Exception
|
||||
end
|
||||
|
||||
class NotExist < Exception
|
||||
end
|
||||
|
||||
# getter file : String
|
||||
# getter default : User?
|
||||
# @list : Hash(String, User)
|
||||
|
||||
include YAML::Serializable
|
||||
property file : String
|
||||
property default : User?
|
||||
property list : Hash(String, User)
|
||||
|
||||
def initialize(@file, @default : User? = nil)
|
||||
@list = {} of String => User
|
||||
# TODO: set UNIX permissions
|
||||
File.touch(@file)
|
||||
end
|
||||
|
||||
# read the users from the file (erase the modifications !)
|
||||
def load!
|
||||
if File.exists?(@file) && (new_users = Users.from_yaml(File.read(@file)) rescue nil)
|
||||
@list = new_users.list
|
||||
@default = new_users.default
|
||||
# @file = new_users.file
|
||||
else
|
||||
@list = {} of String => User
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# save the users into the file
|
||||
def save!
|
||||
File.write @file, self.to_yaml
|
||||
end
|
||||
|
||||
# add an user to the list
|
||||
def add(u : User)
|
||||
raise AlreadyExist.new "User #{u.name} already exists" if (@list[u.name]?)
|
||||
@list[u.name] = u
|
||||
self
|
||||
end
|
||||
|
||||
# Removes an user from the list
|
||||
# @see #delete(String)
|
||||
def delete(u : User)
|
||||
delete u.name
|
||||
self
|
||||
end
|
||||
|
||||
# Remove an user from the list
|
||||
def delete(name : String)
|
||||
raise NotExist.new "User #{name} is not in the list" if (!@list[name]?)
|
||||
@list.delete name
|
||||
self
|
||||
end
|
||||
|
||||
# replace an entry
|
||||
def update(name : String, u : User)
|
||||
raise NotExist.new "User #{name} is not in the list" if (!@list[name]?)
|
||||
|
||||
# if the name change
|
||||
if name != u.name
|
||||
add u # if it fails, remove will fail too
|
||||
remove name
|
||||
else
|
||||
@list[u.name] = u
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# find an user based on its name
|
||||
def find(name : String) : User
|
||||
user = @list[name]?
|
||||
raise NotExist.new "User #{name} is not in the list" unless user
|
||||
user
|
||||
end
|
||||
|
||||
# find an user based on its name
|
||||
def find?(name : String) : User?
|
||||
user = @list[name]?
|
||||
end
|
||||
|
||||
def [](name : String) : User
|
||||
find name
|
||||
end
|
||||
|
||||
def []?(name : String) : User?
|
||||
find? name
|
||||
end
|
||||
|
||||
def each
|
||||
@list.each { |_, user| yield user }
|
||||
end
|
||||
|
||||
def map
|
||||
@list.map { |_, user| yield user }
|
||||
end
|
||||
|
||||
def map!
|
||||
@list.map! { |name, user| {name, yield user} }
|
||||
end
|
||||
|
||||
##################
|
||||
# HIGH LEVEL API #
|
||||
##################
|
||||
|
||||
# No file operation.
|
||||
#
|
||||
# Finds an user by its name and check the password.
|
||||
#
|
||||
# Returns nil if it fails
|
||||
def auth?(name : String, password : String) : User?
|
||||
user = find(name)
|
||||
user && user.password_encrypted.verify(password) ? user : nil
|
||||
end
|
||||
|
||||
def auth_token?(name : String, token : String) : User?
|
||||
user = find?(name)
|
||||
user && user.token == token ? user : nil
|
||||
end
|
||||
|
||||
# Operation read (erase the internal list).
|
||||
#
|
||||
# see `#auth?`
|
||||
def auth!(name : String, password : String) : User?
|
||||
self.load!
|
||||
auth?(name, password)
|
||||
end
|
||||
|
||||
def auth_token!(name : String, token : String) : User?
|
||||
self.load!
|
||||
auth_token?(name, token)
|
||||
end
|
||||
|
||||
# Operation read and write (erase the internal list and the file)
|
||||
#
|
||||
# Registers a new user by create a new `User` with `name`, `password` and `groups`
|
||||
# and then update the file with the new list of users.
|
||||
def register!(name : String, password : String, groups : Array(String) = %w(user))
|
||||
user = User.new(name, password, groups).encrypt!
|
||||
self.transaction! do |users|
|
||||
users.add user
|
||||
end
|
||||
user
|
||||
end
|
||||
end
|
||||
|
||||
# file = "/tmp/users"
|
||||
# File.touch(file)
|
||||
# include Wikicr
|
||||
# users = Users.new(file)
|
||||
# users.load!
|
||||
# pp users
|
||||
# user = User.new("arthur", "passwd", %w(admin,user)).encrypt
|
||||
# users.add user
|
||||
# users.save!
|
||||
# p users
|
||||
# pp Crypto::Bcrypt::Password.new(user.password) == "passwd"
|
||||
# pp users.auth?("arthur", "passwd")
|
||||
# pp users.auth?("arthur", "passwdx")
|
||||
# pp users.auth?("arthurx", "passwd") # raise here
|
|
@ -1,202 +0,0 @@
|
|||
require "markd"
|
||||
require "../page/index"
|
||||
|
||||
module Wikicr::MarkdPatch
|
||||
module Rule
|
||||
WIKI_TAG_OPENNER = /(\{\{|<<)/
|
||||
WIKI_TAG_CLOSER = /(\}\}|>>)/
|
||||
|
||||
# default must be matched last because it matches all the other tags
|
||||
WIKI_TAG = {
|
||||
autolink: /^#{WIKI_TAG_OPENNER}(link:)([[:graph:] ]+)#{WIKI_TAG_CLOSER}/i,
|
||||
tag: /^#{WIKI_TAG_OPENNER}(tag:)([[:graph:] ]+)#{WIKI_TAG_CLOSER}/i,
|
||||
table: /^#{WIKI_TAG_OPENNER}(table:)([[:graph:] ]+)#{WIKI_TAG_CLOSER}/i,
|
||||
default: /^#{WIKI_TAG_OPENNER}([[:graph:] ]+)#{WIKI_TAG_CLOSER}/i,
|
||||
}
|
||||
end
|
||||
|
||||
class Parser::Inline < ::Markd::Parser::Inline
|
||||
# patch the autolink <...>
|
||||
# if we match a wiki tag use it, else we can fallback on default markdown behavior
|
||||
private def auto_link(node : ::Markd::Node)
|
||||
# puts "> Parser::Inline.auto_link Here you go <"
|
||||
begin
|
||||
wiki_tag(node, raise_when_no_match: true)
|
||||
rescue
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# generate a wiki tag if matching a rule
|
||||
private def wiki_tag(node : ::Markd::Node, raise_when_no_match : Bool = false)
|
||||
if text = match(Rule::WIKI_TAG[:tag])
|
||||
node.append_child(wiki_keyword(text))
|
||||
elsif text = match(Rule::WIKI_TAG[:autolink])
|
||||
node.append_child(wiki_internal_link(text))
|
||||
elsif text = match(Rule::WIKI_TAG[:default])
|
||||
node.append_child(wiki_internal_link(text, prefix: 0))
|
||||
else
|
||||
raise "No Match" if raise_when_no_match
|
||||
node
|
||||
end
|
||||
end
|
||||
|
||||
# generate a keyword to group pages
|
||||
# TODO: link to a wiki page that generate a list of all pages using this keyword
|
||||
private def wiki_keyword(text : String, prefix : Int = 4) : ::Markd::Node
|
||||
# puts "> Parser::Inline.wiki Here you go <"
|
||||
input_tags = text[(2 + prefix)..-3].strip
|
||||
tags_node = ::Markd::Node.new(::Markd::Node::Type::HTMLInline)
|
||||
input_tags.split(' ').each do |input_tag|
|
||||
puts "page context.tags = #{page_context.tags}"
|
||||
page_context.tags << input_tag
|
||||
puts "added << #{input_tag}, now #{page_context.tags}"
|
||||
tags_node.text += "<a class=\"badge badge-primary\" href=\"/tags/#{input_tag}\">#{input_tag}</a>"
|
||||
end
|
||||
tags_node.text += "\n"
|
||||
|
||||
tags_node
|
||||
end
|
||||
|
||||
# generate an internal link using the page_index to find a page
|
||||
# the page is related to the page context (option of the parser)
|
||||
# if the page is not found it will link to a new page based on the find algorithm
|
||||
private def wiki_internal_link(text : String, prefix : Int = 5) : ::Markd::Node
|
||||
# puts "> Parser::Inline.wiki Here you go <"
|
||||
input_text = text[(2 + prefix)..-3]
|
||||
input_array = input_text.split('|', 2)
|
||||
target_page =
|
||||
if input_array.size == 2
|
||||
# we have a {{title|url}}
|
||||
input_title = input_array[0]
|
||||
input_url = input_array[1]
|
||||
page_index.one_by_url input_url, page_context, input_title
|
||||
else
|
||||
# we have a {{title}} only
|
||||
page_index.one_by_title_or_url input_text, page_context
|
||||
end
|
||||
node = ::Markd::Node.new(::Markd::Node::Type::Link)
|
||||
node.data["title"] = target_page.title
|
||||
node.data["destination"] = target_page.url
|
||||
node.append_child(text(target_page.title))
|
||||
node
|
||||
end
|
||||
|
||||
# rewrite to add {
|
||||
private def process_line(node : Node)
|
||||
char = char_at?(@pos)
|
||||
|
||||
return false unless char && char != Char::ZERO
|
||||
|
||||
res = case char
|
||||
when '\n'
|
||||
newline(node)
|
||||
when '\\'
|
||||
backslash(node)
|
||||
when '`'
|
||||
backtick(node)
|
||||
when '*', '_'
|
||||
handle_delim(char, node)
|
||||
when '\'', '"'
|
||||
@options.smart? && handle_delim(char, node)
|
||||
when '['
|
||||
open_bracket(node)
|
||||
when '!'
|
||||
bang(node)
|
||||
when ']'
|
||||
close_bracket(node)
|
||||
when '<'
|
||||
auto_link(node) || html_tag(node)
|
||||
when '&'
|
||||
entity(node)
|
||||
when '{'
|
||||
wiki_tag(node) || string(node)
|
||||
else
|
||||
string(node)
|
||||
end
|
||||
|
||||
unless res
|
||||
@pos += 1
|
||||
node.append_child(text(char))
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# rewritte to add {
|
||||
private def main_char?(char)
|
||||
case char
|
||||
when '\n', '`', '[', ']', '\\', '!', '<', '&', '*', '_', '\'', '"', '{'
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def page_index
|
||||
@options.as(Options).page_index
|
||||
end
|
||||
|
||||
def page_context
|
||||
@options.as(Options).page_context
|
||||
end
|
||||
end
|
||||
|
||||
class Parser::Block < ::Markd::Parser::Block
|
||||
def initialize(@options : Options)
|
||||
super(@options)
|
||||
@inline_lexer = Parser::Inline.new(@options.as(Options))
|
||||
end
|
||||
end
|
||||
|
||||
class Options < ::Markd::Options
|
||||
property page_index : Wikicr::Page::Index
|
||||
property page_context : Wikicr::Page
|
||||
property parse_tags : Bool
|
||||
|
||||
def initialize(
|
||||
@time = false,
|
||||
@gfm = false,
|
||||
@toc = true,
|
||||
@smart = false,
|
||||
@source_pos = false,
|
||||
@safe = false,
|
||||
@prettyprint = false,
|
||||
@base_url = nil,
|
||||
@page_index = nil,
|
||||
@page_context = nil,
|
||||
@parse_tags = false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
module Parser
|
||||
include ::Markd::Parser
|
||||
|
||||
def self.parse(source : String, options = Options.new)
|
||||
Block.parse(source, options)
|
||||
end
|
||||
end
|
||||
|
||||
include Markd
|
||||
|
||||
def self.to_html(source : String, options = Options.new) : String
|
||||
return "" if source.empty?
|
||||
document = Parser.parse(source, options)
|
||||
renderer = HTMLRenderer.new(options)
|
||||
renderer.render(document)
|
||||
end
|
||||
|
||||
def self.to_html(
|
||||
input : String,
|
||||
context : Wikicr::Page,
|
||||
index : Wikicr::Page::Index,
|
||||
parse_tags : Bool = false
|
||||
) : String
|
||||
context.tags = [] of String # reset the tags first
|
||||
to_html(
|
||||
input,
|
||||
Options.new(page_index: index, page_context: context, parse_tags: parse_tags),
|
||||
)
|
||||
end
|
||||
end
|
|
@ -1,3 +1,3 @@
|
|||
module Wikicr
|
||||
VERSION = {{ `git tag|sort -h`.split("\n")[-2] }}
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
h1.page-header ACLs management
|
||||
|
||||
h2 Create a new ACL
|
||||
form#acl-create method="post" action="/admin/acls/create"
|
||||
fieldset
|
||||
div.input-group.col-xs-12
|
||||
label Group
|
||||
input.form-control name="group" type="text" required="true"
|
||||
div.input-group.col-xs-12
|
||||
label Path
|
||||
input.form-control name="path" type="text" required="true"
|
||||
div.input-group.col-xs-12
|
||||
label Permission
|
||||
select.form-control name="perm" value="None" required="true"
|
||||
option value="none" None
|
||||
option value="read" Read
|
||||
option value="write" Write
|
||||
div.input-group
|
||||
input.form-control.btn.btn-md.btn-success name="create" type="submit" value="create"
|
||||
|
||||
h2 Manage existing ACL
|
||||
table.table.table-condensed
|
||||
thead
|
||||
tr
|
||||
th Concerned
|
||||
th Path
|
||||
th Permission
|
||||
th Action
|
||||
tbody
|
||||
- acls.groups.each do |_, group|
|
||||
- group.permissions.each do |path, perm|
|
||||
tr
|
||||
td.col-xs-2 = group.name
|
||||
td.col-xs-6 = path.to_s
|
||||
- case perm.to_s
|
||||
- when "Write"
|
||||
td.col-xs-1.bg-success = perm.to_s
|
||||
- when "Read"
|
||||
td.col-xs-1.bg-info = perm.to_s
|
||||
- when "None"
|
||||
td.col-xs-1.bg-warning = perm.to_s
|
||||
td.col-xs-3
|
||||
form.col-xs-3#admin-acl method="post" action="/admin/acls/update"
|
||||
input type="hidden" name="group" value=group.name
|
||||
input type="hidden" name="path" value=path.to_s
|
||||
button.btn.btn-xs.btn-success type="submit" name="change" value="write"
|
||||
span Write
|
||||
form.col-xs-3#admin-acl method="post" action="/admin/acls/update"
|
||||
input type="hidden" name="group" value=group.name
|
||||
input type="hidden" name="path" value=path.to_s
|
||||
button.btn.btn-xs.btn-info type="submit" name="change" value="read"
|
||||
Span Read
|
||||
form.col-xs-3#admin-acl method="post" action="/admin/acls/update"
|
||||
input type="hidden" name="group" value=group.name
|
||||
input type="hidden" name="path" value=path.to_s
|
||||
button.btn.btn-xs.btn-warning type="submit" name="change" value="none"
|
||||
Span None
|
||||
form.col-xs-3#admin-acl method="post" action="/admin/acls/delete"
|
||||
input type="hidden" name="group" value=group.name
|
||||
input type="hidden" name="path" value=path.to_s
|
||||
button.btn.btn-xs.btn-danger type="submit" name="change" value="delete"
|
||||
Span Delete
|
|
@ -1,35 +0,0 @@
|
|||
h1.page-header Users Management
|
||||
|
||||
h2 Create a new User
|
||||
form#register-new-user method="post" action="/admin/users/create"
|
||||
fieldset
|
||||
legend
|
||||
| Admin
|
||||
div.input-group.col-xs-12
|
||||
label Username
|
||||
input.form-control name="username" type="text"
|
||||
div.input-group.col-xs-12
|
||||
label Password
|
||||
input.form-control name="password" type="password"
|
||||
div.input-group.col-xs-12
|
||||
label Groups
|
||||
input.form-control name="groups" type="text"
|
||||
div.input-group
|
||||
input.form-control.btn.btn-md.btn-success name="register" type="submit" value="register"
|
||||
|
||||
h2 Manage existing users
|
||||
table.table.table-condensed
|
||||
thead
|
||||
tr
|
||||
th Username
|
||||
th Groups
|
||||
th Action
|
||||
tbody
|
||||
- users.each do |user|
|
||||
tr
|
||||
td= user.name
|
||||
td= user.groups.join(", ")
|
||||
td
|
||||
form method="post" action="/admin/users/delete"
|
||||
input type="hidden" name="username" value=user.name
|
||||
input.form-control.btn.btn-md.btn-danger name="delete" type="submit" value="delete"
|
40
src/views/layout.html.slang
Normal file
40
src/views/layout.html.slang
Normal file
|
@ -0,0 +1,40 @@
|
|||
html
|
||||
head
|
||||
title
|
||||
= locals[:title]
|
||||
/ jQuery
|
||||
script src="/assets/javascript/jquery-3.2.1.min.js"
|
||||
/ Bootstrap
|
||||
link rel="stylesheet" href="/assets/stylesheet/bootstrap.min.css"
|
||||
link rel="stylesheet" href="/assets/stylesheet/bootstrap-theme.min.css"
|
||||
script src="/assets/javascript/bootstrap.min.js"
|
||||
/ Custom
|
||||
link rel="stylesheet" href="/assets/stylesheet/base.css"
|
||||
script src="/assets/javascript/base.js"
|
||||
|
||||
/ link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"
|
||||
/ link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"
|
||||
/ script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"
|
||||
body
|
||||
nav
|
||||
.pull-left
|
||||
a.btn.btn-md href="/" Home
|
||||
a.btn.btn-md href="/sitemap" Sitemap
|
||||
- if env.session.string?("username")
|
||||
= env.session.string("username")
|
||||
- else
|
||||
a.btn.btn-md href="/users/register" Register
|
||||
a.btn.btn-md href="/users/login" Login
|
||||
.pull-right
|
||||
form#search-page action="/pages/search" method="get" role="form"
|
||||
.form-inline
|
||||
input.form-control name="q" type="text" placeholder="Search query"
|
||||
button.btn.btn-md
|
||||
span.glyphicon.glyphicon-search
|
||||
| Search
|
||||
div.container
|
||||
.col-xs-12
|
||||
h1.small.page-header
|
||||
| #{locals[:title]}
|
||||
#content
|
||||
== content
|
|
@ -1,38 +0,0 @@
|
|||
/ - active = context.request.path == "/" ? "active" : ""
|
||||
/ a class="nav-item #{active}" href="/" Home
|
||||
nav class="navbar navbar-default"
|
||||
.col-xs-6
|
||||
.pull-left
|
||||
- if user_signed_in?
|
||||
a.btn.btn-md href="/" = current_user.name
|
||||
a.btn.btn-md href="/" Home
|
||||
a.btn.btn-md href="/sitemap" Sitemap
|
||||
- if !user_signed_in?
|
||||
a.btn.btn-md href="/users/register" Register
|
||||
a.btn.btn-md href="/users/login" Login
|
||||
- if Wikicr::ACL.permitted?(current_user, "/admin/users", Acl::Perm::Read)
|
||||
a.btn.btn-md href="/admin/users" Users
|
||||
- if Wikicr::ACL.permitted?(current_user, "/admin/acls", Acl::Perm::Read)
|
||||
a.btn.btn-md href="/admin/acls" ACLs
|
||||
.col-xs-6
|
||||
.pull-right
|
||||
form#search-page action="/pages/search" method="get" role="form"
|
||||
.input-group.input-group-md
|
||||
input.input-md.form-control name="q" placeholder="Search query" autocomplete="on" list="search-list"
|
||||
datalist#search-list
|
||||
- Wikicr::PAGES.entries.values.each do |entry|
|
||||
option value=entry.url = entry.title
|
||||
span.input-group-btn
|
||||
button.btn.btn-md
|
||||
span.glyphicon.glyphicon-search
|
||||
| Search
|
||||
|
||||
nav(aria-label="breadcrumb")
|
||||
ol.breadcrumb
|
||||
li.breadcrumb-item.active
|
||||
| History
|
||||
- history.each do |page_url|
|
||||
li.breadcrumb-item
|
||||
a href=page_url
|
||||
- root_page = Wikicr::Page.new "/"
|
||||
= Wikicr::PAGES.load!.one_by_url(page_url[7..-1], root_page).title
|
|
@ -1,34 +0,0 @@
|
|||
doctype html
|
||||
html
|
||||
head
|
||||
title Wikicr
|
||||
meta charset="utf-8"
|
||||
meta http-equiv="X-UA-Compatible" content="IE=edge"
|
||||
meta name="viewport" content="width=device-width, initial-scale=1"
|
||||
|
||||
/ jQuery
|
||||
script src="/assets/javascript/jquery-3.2.1.min.js"
|
||||
/ FontAwesome
|
||||
link rel="stylesheet" href="/assets/stylesheet/font-awesome.min.css"
|
||||
/ Bootstrap
|
||||
link rel="stylesheet" href="/assets/stylesheet/bootstrap.min.css"
|
||||
link rel="stylesheet" href="/assets/stylesheet/bootstrap-theme.min.css"
|
||||
- if ENV["INVERT_THEME"]? == "true"
|
||||
link rel="stylesheet" href="/assets/stylesheet/invert.css"
|
||||
script src="/assets/javascript/bootstrap.min.js"
|
||||
/ Custom
|
||||
link rel="stylesheet" href="/assets/stylesheet/base.css"
|
||||
script src="/assets/javascript/base.js"
|
||||
|
||||
body
|
||||
#navbar
|
||||
== render_template "layouts/_nav.slang"
|
||||
#main
|
||||
.container
|
||||
.row
|
||||
- flash.each do |key, value|
|
||||
div class="alert alert-#{ key }"
|
||||
p = flash[key]
|
||||
.row
|
||||
#content.col-sm-12
|
||||
== content
|
8
src/views/pages/edit.html.slang
Normal file
8
src/views/pages/edit.html.slang
Normal file
|
@ -0,0 +1,8 @@
|
|||
form#edit-page method="post"
|
||||
fieldset.col-xs-6
|
||||
legend
|
||||
| New wiki page: #{locals[:title]}
|
||||
div.input-group.col-xs-12
|
||||
textarea.form-control.textarea-resize name="body" type="body" = locals[:body]
|
||||
div.input-group
|
||||
input.form-control.btn.btn-md.btn-success name="save" type="submit" value="save"
|
|
@ -1,9 +0,0 @@
|
|||
h1.page-header
|
||||
| Edit: #{page.title}
|
||||
|
||||
form#edit-page method="post"
|
||||
fieldset.col-xs-12
|
||||
div.input-group.col-xs-12
|
||||
textarea.form-control.textarea-resize name="body" type="body" = body
|
||||
div.input-group.col-xs-12
|
||||
input.form-control.btn.btn-block.btn-md.btn-success name="save" type="submit" value="save"
|
5
src/views/pages/show.html.slang
Normal file
5
src/views/pages/show.html.slang
Normal file
|
@ -0,0 +1,5 @@
|
|||
.col-xs-8
|
||||
== locals[:body_html]
|
||||
.col-xs-4
|
||||
a.btn.btn-md href="/pages/#{locals[:display_path]}?edit"
|
||||
| Edit
|
|
@ -1,40 +0,0 @@
|
|||
#page-body.col-xs-12
|
||||
#page-body-meta.pull-right.col-xs-12.col-sm-4
|
||||
- if (index_entry = Wikicr::PAGES[page]?) && (!index_entry.toc.empty?)
|
||||
#page-table-of-content
|
||||
span.page-body-meta-title Table of Contents
|
||||
== add_toc index_entry.toc
|
||||
div
|
||||
- if Wikicr::ACL.permitted? current_user, page.real_url, Acl::Perm::Write
|
||||
#page-edit.col-xs-12
|
||||
a.btn.btn-xs.btn-block.btn-warning href="/pages/#{page.url}?edit"
|
||||
| Edit
|
||||
div
|
||||
#page-rename.col-xs-12
|
||||
form#rename-page method="post"
|
||||
fieldset
|
||||
.input-group.input-group-xs.col-xs-12
|
||||
input.input-sm.form-control name="new_path" type="text" placeholder="New url"
|
||||
.input-group.input-group-xs.col-xs-12
|
||||
button.btn.btn-xs.btn-block.btn-success name="rename" type="submit" value="rename" form="rename-page" rename
|
||||
div
|
||||
#page-permissions.col-xs-12
|
||||
span.page-body-meta-title Permissions
|
||||
p
|
||||
dl
|
||||
dt.text-center
|
||||
| Read
|
||||
dd.text-center
|
||||
- groups_read.each do |group|
|
||||
span.label.label-default
|
||||
| #{group}
|
||||
|
|
||||
dt.text-center
|
||||
| Write
|
||||
dd.text-center
|
||||
- groups_write.each do |group|
|
||||
span.label.label-default
|
||||
| #{group}
|
||||
|
|
||||
#page-body-html
|
||||
== body_html
|
|
@ -1,2 +0,0 @@
|
|||
h1.page-header Sitemap
|
||||
== add_page pages
|
|
@ -1 +0,0 @@
|
|||
== toc
|
1
src/views/sitemap.html.slang
Normal file
1
src/views/sitemap.html.slang
Normal file
|
@ -0,0 +1 @@
|
|||
== add_page(locals[:pages])
|
|
@ -1,5 +1,3 @@
|
|||
h1.page-header Login
|
||||
|
||||
form#login method="post"
|
||||
fieldset
|
||||
legend
|
||||
|
@ -8,7 +6,6 @@ form#login method="post"
|
|||
label Username
|
||||
input.form-control name="username" type="text"
|
||||
div.input-group.col-xs-12
|
||||
label Password
|
||||
input.form-control name="password" type="password"
|
||||
div.input-group
|
||||
input.form-control.btn.btn-md.btn-success name="login" type="submit" value="login"
|
11
src/views/users/register.html.slang
Normal file
11
src/views/users/register.html.slang
Normal file
|
@ -0,0 +1,11 @@
|
|||
form#register method="post"
|
||||
fieldset
|
||||
legend
|
||||
| Register
|
||||
div.input-group.col-xs-12
|
||||
label Username
|
||||
input.form-control name="username" type="text"
|
||||
div.input-group.col-xs-12
|
||||
input.form-control name="password" type="password"
|
||||
div.input-group
|
||||
input.form-control.btn.btn-md.btn-success name="register" type="submit" value="register"
|
|
@ -1,14 +0,0 @@
|
|||
h1.page-header Register
|
||||
|
||||
form#register method="post"
|
||||
fieldset
|
||||
legend
|
||||
| Register
|
||||
div.input-group.col-xs-12
|
||||
label Username
|
||||
input.form-control name="username" type="text"
|
||||
div.input-group.col-xs-12
|
||||
label Password
|
||||
input.form-control name="password" type="password"
|
||||
div.input-group
|
||||
input.form-control.btn.btn-md.btn-success name="register" type="submit" value="register"
|
|
@ -1,12 +1,14 @@
|
|||
require "markd"
|
||||
require "yaml"
|
||||
require "markdown"
|
||||
# require "option_parser"
|
||||
|
||||
require "kemal"
|
||||
require "kemal-session"
|
||||
require "kemal-flash"
|
||||
require "kilt/slang"
|
||||
require "git"
|
||||
|
||||
require "./version"
|
||||
require "./lib/_init"
|
||||
require "./lib"
|
||||
require "./controllers"
|
||||
|
||||
require "./controllers/application_controller"
|
||||
require "../config/application"
|
||||
puts "Wiki is written on #{Wikicr::OPTIONS.basedir}"
|
||||
Kemal.run
|
||||
|
|
Loading…
Reference in New Issue
Block a user