├── .default.env ├── .github ├── matchers │ ├── phpcs.json │ └── phpunit.json └── workflows │ └── checks.yml ├── .gitignore ├── .htaccess ├── API.md ├── LICENSE ├── MANUAL.md ├── Makefile ├── README.md ├── RENDER.md ├── UI.md ├── app ├── class │ ├── Application.php │ ├── Bookmark.php │ ├── Clock.php │ ├── Color.php │ ├── Config.php │ ├── Controller.php │ ├── Controlleradmin.php │ ├── Controllerapi.php │ ├── Controllerapimedia.php │ ├── Controllerapipage.php │ ├── Controllerapiuser.php │ ├── Controllerapiworkspace.php │ ├── Controllerbookmark.php │ ├── Controllerconnect.php │ ├── Controllerhome.php │ ├── Controllerinfo.php │ ├── Controllermedia.php │ ├── Controllerpage.php │ ├── Controllerprofile.php │ ├── Controllerrandom.php │ ├── Controlleruser.php │ ├── Controllerworkspace.php │ ├── Element.php │ ├── Elementv1.php │ ├── Elementv2.php │ ├── Exception │ │ ├── Database │ │ │ └── Notfoundexception.php │ │ ├── Databaseexception.php │ │ ├── Filesystemexception.php │ │ ├── Filesystemexception │ │ │ ├── Chmodexception.php │ │ │ ├── Fileexception.php │ │ │ ├── Folderexception.php │ │ │ ├── Notfoundexception.php │ │ │ └── Unlinkexception.php │ │ ├── Forbiddenexception.php │ │ └── Missingextensionexception.php │ ├── Flywheel │ │ ├── Formatter │ │ │ └── JSON.php │ │ ├── Predicate.php │ │ ├── Query.php │ │ └── Repository.php │ ├── Folder.php │ ├── Font.php │ ├── Fs.php │ ├── Graph.php │ ├── Header.php │ ├── Item.php │ ├── Logger.php │ ├── Matchable.php │ ├── Media.php │ ├── Mediaopt.php │ ├── Mediaoptlist.php │ ├── Model.php │ ├── Modeladmin.php │ ├── Modelbookmark.php │ ├── Modelconnect.php │ ├── Modeldb.php │ ├── Modelhome.php │ ├── Modelldap.php │ ├── Modelmedia.php │ ├── Modelpage.php │ ├── Modeluser.php │ ├── Opt.php │ ├── Optcode.php │ ├── Optlist.php │ ├── Optmap.php │ ├── Optrandom.php │ ├── Page.php │ ├── Pagev1.php │ ├── Pagev2.php │ ├── Quickcss.php │ ├── Route.php │ ├── Routes.php │ ├── Servicefont.php │ ├── Servicepostprocess.php │ ├── Servicerender.php │ ├── Servicerenderv1.php │ ├── Servicerenderv2.php │ ├── Servicerss.php │ ├── Servicesession.php │ ├── Servicetags.php │ ├── Serviceurlchecker.php │ ├── Summary.php │ ├── User.php │ └── Workspace.php ├── fn │ └── fn.php └── view │ └── templates │ ├── admin.php │ ├── alertcommandnotfound.php │ ├── alertexistnot.php │ ├── alertform.php │ ├── alertlayout.php │ ├── alertnotpublished.php │ ├── alertprivate.php │ ├── alertrandom.php │ ├── backtopbar.php │ ├── connect.php │ ├── delete.php │ ├── edit.php │ ├── edithelp.php │ ├── editleftbar.php │ ├── editrightbar.php │ ├── edittabs.php │ ├── edittopbar.php │ ├── footer.php │ ├── forbidden.php │ ├── home.php │ ├── homebookmark.php │ ├── homemenu.php │ ├── homeopt.php │ ├── info.php │ ├── layout.php │ ├── macro_tablesort.php │ ├── media.php │ ├── mediamenu.php │ ├── modallayout.php │ ├── pagepassword.php │ ├── profile.php │ ├── readerlayout.php │ ├── user.php │ └── userconfirmdelete.php ├── assets ├── css │ ├── admin.css │ ├── back.css │ ├── base.css │ ├── connect.css │ ├── edit.css │ ├── fork-awesome.css │ ├── home.css │ ├── info.css │ ├── media.css │ ├── modal.css │ ├── profile.css │ ├── theme │ │ ├── audrey-s-book.css │ │ ├── blue-whale.css │ │ ├── dark-doriphore.css │ │ ├── default.css │ │ ├── funky-freddy.css │ │ ├── fuzzy-flamingo.css │ │ ├── industrial-dream.css │ │ └── soy-n-wasabi.css │ └── user.css └── fonts │ ├── forkawesome-webfont.eot │ ├── forkawesome-webfont.svg │ ├── forkawesome-webfont.ttf │ ├── forkawesome-webfont.woff │ └── forkawesome-webfont.woff2 ├── codecov.yaml ├── composer.json ├── composer.lock ├── index.php ├── package-lock.json ├── package.json ├── phpcs.xml ├── phpstan.neon ├── phpunit.xml ├── src ├── edit.js ├── fn │ └── fn.js ├── graph.js ├── home.js ├── map.js ├── media.js ├── pagemap.js └── sentry.js └── tests ├── .gitattributes ├── FilesTest.php ├── LoggerTest.php ├── Servicerenderv1Test.php ├── Servicerenderv2Test.php ├── SummaryTest.php ├── data ├── Servicerenderv1Test │ ├── body-test.html │ ├── body-test.json │ ├── date-time-test.html │ ├── date-time-test.json │ ├── empty-test.html │ ├── empty-test.json │ ├── external-links-test.html │ ├── external-links-test.json │ ├── markdown-test-2.html │ ├── markdown-test-2.json │ ├── markdown-test.html │ └── markdown-test.json └── Servicerenderv2Test │ ├── body-test-v2.html │ ├── body-test-v2.json │ ├── date-time-test-v2.html │ ├── date-time-test-v2.json │ ├── empty-test-v2.html │ ├── empty-test-v2.json │ ├── external-links-test-v2.html │ ├── external-links-test-v2.json │ ├── markdown-test-v2.html │ └── markdown-test-v2.json ├── fixtures └── README.md └── fn.php /.default.env: -------------------------------------------------------------------------------- 1 | # .env 2 | 3 | # GitHub token used by release-it to publish releases 4 | GITHUB_TOKEN= 5 | 6 | # Sentry variables used to publish releases to Sentry 7 | SENTRY_AUTH_TOKEN= 8 | SENTRY_ORG=vincent-peugnet 9 | SENTRY_PROJECT=wcms 10 | -------------------------------------------------------------------------------- /.github/matchers/phpcs.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "phpcs", 5 | "fileLocation": "relative", 6 | "pattern": [ 7 | { 8 | "regexp": "^\"(.+)\",(\\d+),(\\d+),(.+),\"(.+)\",(.+),(\\d+),(\\d+)$", 9 | "file": 1, 10 | "line": 2, 11 | "column": 3, 12 | "severity": 4, 13 | "message": 5, 14 | "code": 6 15 | } 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.github/matchers/phpunit.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "phpunit", 5 | "fileLocation": "absolute", 6 | "pattern": [ 7 | { 8 | "regexp": "^\\d+\\)\\s.*$" 9 | }, 10 | { 11 | "regexp": "^(.*)$", 12 | "message": 1 13 | }, 14 | { 15 | "regexp": "^\\s*$" 16 | }, 17 | { 18 | "regexp": "^(.*):(\\d+)$", 19 | "file": 1, 20 | "line": 2 21 | } 22 | ] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | .env 3 | *.log 4 | assets/atom/* 5 | assets/render/* 6 | assets/global/* 7 | assets/manual/* 8 | assets/css/tagcolors.css 9 | assets/js/* 10 | build/ 11 | database/* 12 | media/* 13 | render/ 14 | dist/ 15 | vendor/* 16 | node_modules/ 17 | config.json 18 | error_log 19 | .BUILDDATE 20 | VERSION 21 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | # prevent Apache from adding trailing slash for names that match folder 2 | # in order to avoid conflict with Controllerpage::pagepermanentredirect() 3 | # and 'assets' and 'media' folder 4 | DirectorySlash off 5 | RewriteEngine on 6 | # everything that does not contain asssets|media 7 | RewriteCond %{REQUEST_URI} !^(.*)/(assets|media)/ [OR] 8 | # or that isn't a file 9 | RewriteCond %{REQUEST_FILENAME} !-f 10 | # is redirect to index 11 | RewriteRule . index.php [L] 12 | 13 | -------------------------------------------------------------------------------- /RENDER.md: -------------------------------------------------------------------------------- 1 | W render engine scheme 2 | ====================== 3 | 4 | This diagram represent W rendering chain. 5 | 6 | 7 | ```mermaid 8 | flowchart TD 9 | 10 | 0A(Head generation) --> 11 | 0rss(RSS feed declaration) --> 3B 12 | 13 | 2A[[Body]] --> 14 | 2B(W inclusion) -------> 15 | 2C((Element inclusion)) --> 2D 16 | subgraph "post inclusion parser" 17 | 2D(Summary) --> 18 | 2rss(RSS detection) --> 19 | 2H(Wiki links) --> 20 | 2I(Link and media analysis) --> 21 | 2pp(check for post render actions) 22 | end 23 | 2pp --> 24 | 3B((Head and Body gathering)) --> 25 | 3C[[Rendered HTML]] --> 4c 26 | subgraph "post render actions" 27 | 4c(counters) --> 28 | 4j(js vars) 29 | end 30 | 4j --> 5[\served web page/] 31 | 32 | 33 | 1A[[Element]] --> 34 | 1B(W inclusion) --> 35 | 1C(every link*) --> 36 | 1D(Markdown) --> 1E 37 | subgraph "post MD parser" 38 | 1E(header ID) --> 39 | 1F(URL linker) --> 40 | 1G(HTML tag*) 41 | end 42 | 1G --> 2C 43 | 44 | 1E -. "send TOC structure" .-> 2D 45 | 2rss -. "send rss links" .-> 0rss 46 | 2pp -. trigger post render action .-> 4c 47 | ``` 48 | 49 | - *every link: rendering option that transform every word as a link 50 | - *HTML tag: [rendering option](MANUAL.md#html-tags) that does not print Element's corresponding HTML tags (only for pages V1) 51 | 52 | 53 | 54 | 55 | ## W inclusions 56 | 57 | List of W inclusions 58 | 59 | 1. replace `%DATE%`, `%DATEMODIF%`, `%TIME%`, `%TIMEMODIF%` codes 60 | 1. replace `%THUMBNAIL%` code 61 | 1. replace `%PAGEID%` and `%ID%` code 62 | 1. replace `%URL%` code 63 | 1. replace `%PATH%` code 64 | 1. replace `%TITLE%` code 65 | 1. replace `%DESCRIPTION%` code 66 | 1. replace `%LIST%` code 67 | 1. replace `%MEDIA%` code 68 | 1. replace `%MAP%` code 69 | 1. replace `%RANDOM%` code 70 | 1. replace `%AUTHORS%` code 71 | 1. replace `%CONNECT%` code 72 | 73 | The point of doing those inclusions early is to be before __Header ID__ parser. That way, when they are used inside HTML headings, they will generate nicer IDs. 74 | -------------------------------------------------------------------------------- /app/class/Bookmark.php: -------------------------------------------------------------------------------- 1 | hydrateexception($datas); 39 | } 40 | 41 | public function init( 42 | string $id, 43 | string $query, 44 | string $icon = '⭐', 45 | string $name = '', 46 | string $description = '' 47 | ) { 48 | $this->setid($id); 49 | $this->setquery($query); 50 | $this->seticon($icon); 51 | $this->setname($name); 52 | $this->setdescription($description); 53 | } 54 | 55 | public function ispublic(): bool 56 | { 57 | return empty($this->user); 58 | } 59 | 60 | public function ispublished(): bool 61 | { 62 | return $this->published; 63 | } 64 | 65 | 66 | // _____________________________ G E T __________________________________ 67 | 68 | 69 | public function id() 70 | { 71 | return $this->id; 72 | } 73 | 74 | public function name(): string 75 | { 76 | return $this->name; 77 | } 78 | 79 | public function description(): string 80 | { 81 | return $this->description; 82 | } 83 | 84 | public function query() 85 | { 86 | return $this->query; 87 | } 88 | 89 | public function icon() 90 | { 91 | return $this->icon; 92 | } 93 | 94 | public function user(): string 95 | { 96 | return $this->user; 97 | } 98 | 99 | public function published(): bool 100 | { 101 | return $this->published; 102 | } 103 | 104 | public function ref(): ?string 105 | { 106 | return $this->ref; 107 | } 108 | 109 | // _____________________________ S E T __________________________________ 110 | 111 | public function setid($id): bool 112 | { 113 | if (is_string($id)) { 114 | $id = Model::idclean($id); 115 | if (!empty($id)) { 116 | $this->id = $id; 117 | return true; 118 | } 119 | } 120 | return false; 121 | } 122 | 123 | public function setname(string $name): bool 124 | { 125 | if (strlen($name) < self::LENGTH_SHORT_TEXT) { 126 | $this->name = strip_tags(trim($name)); 127 | return true; 128 | } else { 129 | return false; 130 | } 131 | } 132 | 133 | public function setdescription(string $description): bool 134 | { 135 | if (strlen($description) < self::LENGTH_SHORT_TEXT) { 136 | $this->description = strip_tags(trim($description)); 137 | return true; 138 | } else { 139 | return false; 140 | } 141 | } 142 | 143 | public function setquery($query) 144 | { 145 | if (is_string($query)) { 146 | $this->query = strip_tags(mb_substr($query, 0, Model::MAX_QUERY_LENGH)); 147 | } 148 | } 149 | 150 | public function seticon($icon) 151 | { 152 | if (is_string($icon)) { 153 | $this->icon = mb_substr(strip_tags($icon), 0, 16); 154 | } 155 | } 156 | 157 | public function setuser($user) 158 | { 159 | if (is_string($user)) { 160 | $this->user = Model::idclean($user); 161 | return true; 162 | } 163 | return false; 164 | } 165 | 166 | public function setpublished(bool $published) 167 | { 168 | $this->published = $published; 169 | } 170 | 171 | /** 172 | * @param string $id ID of reference page 173 | */ 174 | public function setref(string $id): void 175 | { 176 | if (!empty($id) && !Model::idcheck($id)) { 177 | $this->ref = ""; 178 | } 179 | $this->ref = $id; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /app/class/Color.php: -------------------------------------------------------------------------------- 1 | 255 || $r < 0 || $g > 255 || $g < 0 || $b > 255 || $b < 0) { 24 | throw new DomainException("Invalid rgb value: $r, $g, $b to define Color object"); 25 | } 26 | $this->r = $r; 27 | $this->g = $g; 28 | $this->b = $b; 29 | } 30 | 31 | /** 32 | * @return int Luma value from 0 to 255 33 | */ 34 | public function luma(): int 35 | { 36 | return ($this->r * 299 + $this->g * 587 + $this->b * 114) / 1000; 37 | } 38 | 39 | /** 40 | * @return string hexa color code starting with # 41 | */ 42 | public function hexa(): string 43 | { 44 | $hex = '#'; 45 | $hex .= str_pad(dechex($this->r), 2, '0', STR_PAD_LEFT); 46 | $hex .= str_pad(dechex($this->g), 2, '0', STR_PAD_LEFT); 47 | $hex .= str_pad(dechex($this->b), 2, '0', STR_PAD_LEFT); 48 | return $hex; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/class/Controlleradmin.php: -------------------------------------------------------------------------------- 1 | adminmanager = new Modeladmin(); 21 | 22 | if ($this->user->isvisitor()) { 23 | http_response_code(401); 24 | $this->showtemplate('connect', ['route' => 'admin']); 25 | exit; 26 | } 27 | if (!$this->user->isadmin()) { 28 | http_response_code(403); 29 | $this->showtemplate('forbidden'); 30 | exit; 31 | } 32 | } 33 | 34 | public function desktop() 35 | { 36 | $datas['pagelist'] = $this->pagemanager->list(); 37 | $this->mediamanager = new Modelmedia(); 38 | $datas['faviconlist'] = $this->mediamanager->listfavicon(); 39 | $datas['thumbnaillist'] = $this->mediamanager->listthumbnail(); 40 | $datas['themes'] = $this->mediamanager->listthemes(); 41 | 42 | $globalcssfile = Model::GLOBAL_CSS_FILE; 43 | 44 | if (is_file($globalcssfile)) { 45 | $datas['globalcss'] = file_get_contents($globalcssfile); 46 | } else { 47 | $datas['globalcss'] = ""; 48 | } 49 | 50 | try { 51 | $datas['pagetables'] = $this->adminmanager->pagetables(); 52 | } catch (RuntimeException $e) { 53 | Logger::errorex($e); 54 | $datas['pagetables'] = []; 55 | } 56 | 57 | $this->showtemplate('admin', $datas); 58 | } 59 | 60 | public function update() 61 | { 62 | try { 63 | Fs::accessfile(Model::GLOBAL_CSS_FILE, true); 64 | Fs::writefile(Model::GLOBAL_CSS_FILE, $_POST['globalcss'], 0664); 65 | Config::hydrate($_POST); 66 | Config::savejson(); 67 | $this->sendflashmessage("Configuration succesfully updated", self::FLASH_SUCCESS); 68 | } catch (Filesystemexception $e) { 69 | $this->sendflashmessage("Can't write config file or global css file", self::FLASH_ERROR); 70 | } 71 | $this->routedirect('admin'); 72 | } 73 | 74 | public function database() 75 | { 76 | if (!empty($_POST['action'])) { 77 | switch ($_POST['action']) { 78 | case 'duplicate': 79 | if (!empty($_POST['dbsrc']) && !empty($_POST['dbtarget'])) { 80 | $this->adminmanager->copydb($_POST['dbsrc'], $_POST['dbtarget']); 81 | } 82 | break; 83 | case 'select': 84 | if (!empty($_POST['pagetable'])) { 85 | Config::hydrate($_POST); 86 | try { 87 | $this->pagemanager->flushrendercache(); 88 | Config::savejson(); 89 | } catch (RuntimeException $e) { 90 | $this->sendflashmessage( 91 | 'Cannot update Config file : ' . $e->getMessage(), 92 | self::FLASH_ERROR 93 | ); 94 | } 95 | } 96 | break; 97 | } 98 | } 99 | $this->routedirect('admin'); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/class/Controllerapi.php: -------------------------------------------------------------------------------- 1 | getrequestbody(); 46 | try { 47 | $datas = json_decode($json, true, 512, JSON_THROW_ON_ERROR); 48 | } catch (JsonException $e) { 49 | $this->shortresponse(400, "Json decoding error: " . $e->getMessage()); 50 | } 51 | return $datas; 52 | } 53 | 54 | /** 55 | * Response containing a HTTP header code and a message encoded in JSON 56 | * 57 | * @param int $code HTTP response code header 58 | * @param string $message Error message to display 59 | */ 60 | protected function shortresponse(int $code, string $message = ""): never 61 | { 62 | http_response_code($code); 63 | header('Content-type: application/json; charset=utf-8'); 64 | echo json_encode(["message" => $message], JSON_PRETTY_PRINT); 65 | exit; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/class/Controllerapimedia.php: -------------------------------------------------------------------------------- 1 | mediamanager = new Modelmedia(); 18 | } 19 | 20 | /** 21 | * Upload a single file to a target directory. Folders are created automatically. 22 | * It will erase any already existing file. 23 | */ 24 | public function upload(string $path): void 25 | { 26 | if (!$this->user->iseditor()) { 27 | $this->shortresponse(403, 'Unauthorized to upload files'); 28 | } 29 | try { 30 | $file = $this->getrequestbody(); 31 | } catch (Error $e) { 32 | $this->shortresponse(400, 'Error while reading the stream: ' . $e->getMessage()); 33 | } 34 | $path = rawurldecode($path); 35 | $pathinfo = pathinfo($path); 36 | $dirname = Model::MEDIA_DIR . $pathinfo['dirname']; //without trailing slash 37 | try { 38 | Fs::dircheck($dirname, true, 0775); 39 | Fs::writefile(Model::MEDIA_DIR . $path, $file, 0664); 40 | $this->shortresponse(200, 'File successfully uploaded'); 41 | } catch (Filesystemexception $e) { 42 | $this->shortresponse(400, 'Error while saving file: ' . $e->getMessage()); 43 | } 44 | } 45 | 46 | public function delete(string $path): void 47 | { 48 | if (!$this->user->iseditor()) { 49 | $this->shortresponse(403, 'Unauthorized to upload files'); 50 | } 51 | try { 52 | $media = new Media(Model::MEDIA_DIR . $path); 53 | $this->mediamanager->delete($media); 54 | } catch (RuntimeException $e) { 55 | $this->shortresponse(400, 'Error while deleting media file: ' . $e->getMessage()); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/class/Controllerapiuser.php: -------------------------------------------------------------------------------- 1 | user->isadmin()) { 18 | $this->shortresponse(401, 'Access unauthrozed, you need to be admin'); 19 | } 20 | try { 21 | $user = $this->usermanager->get($user); 22 | http_response_code(200); 23 | header('Content-type: application/json; charset=utf-8'); 24 | echo json_encode($user, JSON_PRETTY_PRINT); 25 | } catch (Notfoundexception $e) { 26 | $this->shortresponse(404, 'User not found'); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/class/Controllerapiworkspace.php: -------------------------------------------------------------------------------- 1 | workspace->hydrate($_POST); 11 | $this->servicesession->setworkspace($this->workspace); 12 | } else { 13 | $this->shortresponse(400, "No POST datas recieved"); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/class/Controllerinfo.php: -------------------------------------------------------------------------------- 1 | user->isvisitor()) { 14 | http_response_code(401); 15 | $this->showtemplate('connect', ['route' => 'info']); 16 | exit; 17 | } 18 | } 19 | 20 | public function desktop() 21 | { 22 | if ($this->user->isinvite()) { 23 | $version = getversion(); 24 | $mandir = Model::MAN_RENDER_DIR; 25 | try { 26 | $manual = Fs::readfile("$mandir/manual_$version.html"); 27 | $summary = Fs::readfile("$mandir/summary_$version.html"); 28 | } catch (RuntimeException $e) { 29 | try { 30 | $mansrc = Fs::readfile(Model::MAN_FILE); 31 | $render = new Servicerenderv2($this->router, $this->pagemanager, true); 32 | $manual = $render->rendermanual($mansrc); 33 | 34 | $sum = new Summary(['min' => 2, 'max' => 4, 'sum' => $render->sum()]); 35 | $summary = $sum->sumparser(); 36 | 37 | Fs::folderflush($mandir); 38 | Fs::dircheck($mandir, true, 0775); 39 | 40 | Fs::writefile("$mandir/manual_$version.html", $manual); 41 | Fs::writefile("$mandir/summary_$version.html", $summary); 42 | } catch (RuntimeException $e) { 43 | $manual = '⚠️ Error while trying to access MANUAL.md file.'; 44 | $summary = ''; 45 | } 46 | } 47 | $this->showtemplate('info', ['version' => getversion(), 'manual' => $manual, 'summary' => $summary]); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/class/Controllerprofile.php: -------------------------------------------------------------------------------- 1 | user->isvisitor()) { 16 | http_response_code(401); 17 | $this->showtemplate('connect', ['route' => 'profile']); 18 | exit; 19 | } 20 | } 21 | 22 | public function desktop() 23 | { 24 | try { 25 | $datas['user'] = $this->usermanager->get($this->user); 26 | $this->showtemplate('profile', $datas); 27 | } catch (Notfoundexception $e) { 28 | $this->sendflashmessage($e->getMessage(), self::FLASH_ERROR); 29 | $this->routedirect('home'); 30 | } 31 | } 32 | 33 | public function update() 34 | { 35 | try { 36 | $user = $this->usermanager->get($this->user); 37 | $user->hydrateexception($_POST); 38 | $this->usermanager->update($user); 39 | $this->sendflashmessage('Successfully updated', self::FLASH_SUCCESS); 40 | } catch (Notfoundexception $e) { 41 | $this->sendflashmessage($e->getMessage(), self::FLASH_ERROR); 42 | } catch (RuntimeException $e) { 43 | $this->sendflashmessage( 44 | 'There was a problem when updating preference : ' . $e->getMessage(), 45 | self::FLASH_ERROR 46 | ); 47 | } 48 | $this->routedirect('profile'); 49 | } 50 | 51 | /** 52 | * Update the user's password. 53 | */ 54 | public function password() 55 | { 56 | if ($this->user->isldap()) { 57 | http_response_code(403); 58 | $this->showtemplate('forbidden', ['route' => 'profile']); 59 | exit; 60 | } 61 | 62 | if ( 63 | !isset($_POST['currentpassword']) || 64 | !$this->usermanager->passwordcheck($this->user, $_POST['currentpassword']) 65 | ) { 66 | $this->sendflashmessage("wrong current password", self::FLASH_ERROR); 67 | $this->routedirect('profile'); 68 | } 69 | 70 | if ( 71 | empty($_POST['password1']) || 72 | empty($_POST['password2']) || 73 | $_POST['password1'] !== $_POST['password2'] 74 | ) { 75 | $this->sendflashmessage("passwords does not match", self::FLASH_ERROR); 76 | $this->routedirect('profile'); 77 | } 78 | 79 | if ( 80 | !$this->user->setpassword($_POST['password1']) || 81 | !$this->user->hashpassword() 82 | ) { 83 | $this->sendflashmessage("password is not compatible", self::FLASH_ERROR); 84 | $this->routedirect('profile'); 85 | } 86 | 87 | try { 88 | $this->usermanager->add($this->user); 89 | $this->sendflashmessage('password updated successfully', self::FLASH_SUCCESS); 90 | } catch (Databaseexception $e) { 91 | $this->sendflashmessage($e->getMessage(), self::FLASH_ERROR); 92 | } 93 | $this->routedirect('profile'); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/class/Controllerrandom.php: -------------------------------------------------------------------------------- 1 | optrandom = new Optrandom($_GET); 14 | 15 | try { 16 | $origin = $this->pagemanager->get($this->optrandom->origin()); 17 | 18 | $pages = $this->pagemanager->pagelist(); 19 | $pages = $this->pagemanager->pagetable($pages, $this->optrandom); 20 | unset($pages[$origin->id()]); 21 | $keys = array_intersect_key($pages, array_flip($origin->linkto())); 22 | if (!empty($keys)) { 23 | $page = $pages[array_rand($keys)]; 24 | $this->routedirect('pageread', ['page' => $page->id()]); 25 | } else { 26 | $message = 'Empty set of page'; 27 | } 28 | } catch (RuntimeException $e) { 29 | $message = 'Origin page does not exist'; 30 | } 31 | 32 | if (isset($message)) { 33 | $this->showtemplate('alertrandom', ['message' => $message]); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/class/Controllerworkspace.php: -------------------------------------------------------------------------------- 1 | user->isinvite()) { 10 | $this->workspace->hydrate($_POST); 11 | $this->servicesession->setworkspace($this->workspace); 12 | } 13 | 14 | switch ($_POST['route']) { 15 | case 'pageedit': 16 | if (isset($_POST['page'])) { 17 | $this->routedirect('pageedit', ['page' => $_POST['page']]); 18 | } 19 | break; 20 | 21 | case 'home': 22 | $this->routedirect('home'); 23 | break; 24 | 25 | case 'media': 26 | $this->routedirect('media'); 27 | break; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/class/Element.php: -------------------------------------------------------------------------------- 1 | fullmatch; 43 | } 44 | 45 | public function options(): string 46 | { 47 | return $this->options; 48 | } 49 | 50 | public function everylink(): int 51 | { 52 | return $this->everylink; 53 | } 54 | 55 | public function markdown(): bool 56 | { 57 | return $this->markdown; 58 | } 59 | 60 | public function content(): string 61 | { 62 | return $this->content; 63 | } 64 | 65 | public function minheaderid(): int 66 | { 67 | return $this->minheaderid; 68 | } 69 | 70 | public function maxheaderid(): int 71 | { 72 | return $this->maxheaderid; 73 | } 74 | 75 | public function headerid(): string 76 | { 77 | return $this->headerid; 78 | } 79 | 80 | public function headeranchor(): int 81 | { 82 | return $this->headeranchor; 83 | } 84 | 85 | public function urllinker(): bool 86 | { 87 | return $this->urllinker; 88 | } 89 | 90 | 91 | 92 | 93 | 94 | 95 | // ______________________________________________ S E T ________________________________________________________ 96 | 97 | 98 | public function setfullmatch(string $fullmatch) 99 | { 100 | $this->fullmatch = $fullmatch; 101 | } 102 | 103 | public function setoptions(string $options) 104 | { 105 | if (!empty($options)) { 106 | $this->options = $options; 107 | } 108 | } 109 | 110 | public function seteverylink(int $level) 111 | { 112 | if ($level >= 0 && $level <= 16) { 113 | $this->everylink = $level; 114 | return true; 115 | } else { 116 | return false; 117 | } 118 | } 119 | 120 | public function setmarkdown($markdown) 121 | { 122 | $this->markdown = boolval($markdown); 123 | } 124 | 125 | public function setcontent(string $content) 126 | { 127 | $this->content = $content; 128 | } 129 | 130 | public function setheaderid(string $headerid) 131 | { 132 | if ($headerid == 0) { 133 | $this->headerid = 0; 134 | } else { 135 | preg_match('~([1-6])\-([1-6])~', $headerid, $out); 136 | $this->minheaderid = intval($out[1]); 137 | $this->maxheaderid = intval($out[2]); 138 | } 139 | } 140 | 141 | public function setheaderanchor($headeranchor) 142 | { 143 | if (in_array($headeranchor, self::HEADER_ANCHOR_MODES)) { 144 | $this->headeranchor = (int) $headeranchor; 145 | } 146 | } 147 | 148 | public function seturllinker($urllinker) 149 | { 150 | $this->urllinker = boolval($urllinker); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /app/class/Elementv1.php: -------------------------------------------------------------------------------- 1 | tag = Config::htmltag(); 19 | $this->urllinker = Config::urllinker(); 20 | $this->fullmatch = $fullmatch; 21 | $type = strtolower($type); 22 | if (in_array($type, Pagev1::HTML_ELEMENTS)) { 23 | $this->type = $type; 24 | } else { 25 | throw new DomainException("$type is not a valid Page HTML Element Type"); 26 | } 27 | $this->options = $options; 28 | $this->analyse($pageid); 29 | } 30 | 31 | protected function analyse(string $pageid) 32 | { 33 | if (!empty($this->options)) { 34 | $this->options = str_replace('*', $pageid, $this->options); 35 | parse_str($this->options, $datas); 36 | if (isset($datas['id'])) { 37 | $this->sources = explode(' ', $datas['id']); 38 | } else { 39 | $this->sources = [$pageid]; 40 | } 41 | $this->hydrate($datas); 42 | } else { 43 | $this->sources = [$pageid]; 44 | } 45 | } 46 | 47 | 48 | // ______________________________________________ G E T ________________________________________________________ 49 | 50 | public function sources(): array 51 | { 52 | return $this->sources; 53 | } 54 | 55 | public function type(): string 56 | { 57 | return $this->type; 58 | } 59 | 60 | public function tag(): bool 61 | { 62 | return $this->tag; 63 | } 64 | 65 | public function settag($tag) 66 | { 67 | $this->tag = boolval($tag); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/class/Elementv2.php: -------------------------------------------------------------------------------- 1 | urllinker = Config::urllinker(); 12 | $this->fullmatch = $fullmatch; 13 | $this->id = $pageid; 14 | $this->options = $options; 15 | $this->analyse($pageid); 16 | } 17 | 18 | protected function analyse(string $pageid) 19 | { 20 | parse_str($this->options, $datas); 21 | $this->hydrate($datas); 22 | } 23 | 24 | 25 | 26 | // ______________________________________________ G E T ________________________________________________________ 27 | 28 | public function id(): string 29 | { 30 | return $this->id; 31 | } 32 | 33 | public function setid($id): void 34 | { 35 | $this->id = $id; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/class/Exception/Database/Notfoundexception.php: -------------------------------------------------------------------------------- 1 | operators = array( 10 | '>', '>=', '<', '<=', '==', '===', '!=', '!==', 'IN' 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/class/Flywheel/Query.php: -------------------------------------------------------------------------------- 1 | predicate = new Predicate(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/class/Flywheel/Repository.php: -------------------------------------------------------------------------------- 1 | path, Model::FOLDER_PERMISSION)) { 18 | throw new RuntimeException("error while trying to change permission of database folder: " . $this->path); 19 | } 20 | } 21 | 22 | /** 23 | * Get an array containing the path of all files in this repository 24 | * 25 | * @return array An array, item is a file 26 | */ 27 | public function getAllFiles() 28 | { 29 | $ext = $this->formatter->getFileExtension(); 30 | $files = glob($this->path . DIRECTORY_SEPARATOR . '*.' . $ext); 31 | return $files; 32 | } 33 | 34 | /** 35 | * Get an array containing the id of all files in this repository 36 | * 37 | * @return array An array, item is a id 38 | */ 39 | public function getAllIds() 40 | { 41 | $ext = $this->formatter->getFileExtension(); 42 | return array_map(function ($path) use ($ext) { 43 | return $this->getIdFromPath($path, $ext); 44 | }, $this->getAllFiles()); 45 | } 46 | 47 | protected function write($path, $contents): bool 48 | { 49 | $ret = parent::write($path, $contents); 50 | chmod($path, Model::FILE_PERMISSION); 51 | return $ret; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/class/Folder.php: -------------------------------------------------------------------------------- 1 | name = $name; 19 | $this->childs = $childs; 20 | $this->filecount = $filecount; 21 | $this->path = $path; 22 | $this->deepness = $deepness; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/class/Graph.php: -------------------------------------------------------------------------------- 1 | 'cose', 14 | 'fcose' => 'fcose', 15 | 'cose-bilkent' => 'cose-bilkent', 16 | 'euler' => 'euler', 17 | 'circle' => 'circle', 18 | 'breadthfirst' => 'breadthfirst', 19 | 'concentric' => 'concentric', 20 | 'grid' => 'grid', 21 | 'random' => 'random', 22 | ]; 23 | 24 | /** 25 | * @param mixed[] $datas 26 | */ 27 | public function __construct(array $datas) 28 | { 29 | $this->hydrate($datas); 30 | } 31 | 32 | public function showorphans(): bool 33 | { 34 | return $this->showorphans; 35 | } 36 | 37 | public function showredirection(): bool 38 | { 39 | return $this->showredirection; 40 | } 41 | 42 | public function showexternallinks(): bool 43 | { 44 | return $this->showexternallinks; 45 | } 46 | 47 | public function layout(): string 48 | { 49 | return $this->layout; 50 | } 51 | 52 | public function setshowredirection($showredirection): void 53 | { 54 | $this->showredirection = boolval($showredirection); 55 | } 56 | 57 | public function setshoworphans($showorphans): void 58 | { 59 | $this->showorphans = boolval($showorphans); 60 | } 61 | 62 | public function setshowexternallinks($showexternallinks): void 63 | { 64 | $this->showexternallinks = boolval($showexternallinks); 65 | } 66 | 67 | public function setlayout($layout): void 68 | { 69 | if (key_exists($layout, $this::LAYOUTS)) { 70 | $this->layout = $layout; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/class/Header.php: -------------------------------------------------------------------------------- 1 | id = $id; 22 | $this->level = $level; 23 | $this->title = $title; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/class/Logger.php: -------------------------------------------------------------------------------- 1 | getMessage()} in {$e->getFile()}({$e->getLine()})"; 64 | } 65 | 66 | /** 67 | * Log an error message using printf format. 68 | */ 69 | public static function error(string $msg, ...$args) 70 | { 71 | if (self::$verbosity > 0) { 72 | self::write('ERROR', $msg, $args); 73 | } 74 | } 75 | 76 | /** 77 | * Log a warning message using printf format. 78 | */ 79 | public static function warning(string $msg, ...$args) 80 | { 81 | if (self::$verbosity > 1) { 82 | self::write('WARN', $msg, $args); 83 | } 84 | } 85 | 86 | /** 87 | * Log an info message using printf format. 88 | */ 89 | public static function info(string $msg, ...$args) 90 | { 91 | if (self::$verbosity > 2) { 92 | self::write('INFO', $msg, $args); 93 | } 94 | } 95 | 96 | /** 97 | * Log a debug message using printf format. 98 | */ 99 | public static function debug(string $msg, ...$args) 100 | { 101 | if (self::$verbosity > 3) { 102 | self::write('DEBUG', $msg, $args); 103 | } 104 | } 105 | 106 | /** 107 | * Log an exception as an error. 108 | */ 109 | public static function errorex(Throwable $e, bool $withtrace = false) 110 | { 111 | if (self::$verbosity > 0) { 112 | $msg = self::exceptionmessage($e); 113 | if ($withtrace) { 114 | // TODO: Maybe print a more beautiful stack trace. 115 | $msg .= PHP_EOL . $e->getTraceAsString(); 116 | } 117 | self::write('ERROR', $msg); 118 | } 119 | } 120 | 121 | /** 122 | * Log an exception as a warning. 123 | */ 124 | public static function warningex(Throwable $e) 125 | { 126 | if (self::$verbosity > 1) { 127 | self::write('WARN', self::exceptionmessage($e)); 128 | } 129 | } 130 | 131 | /** 132 | * Log an exception as an info. 133 | */ 134 | public static function infoex(Throwable $e) 135 | { 136 | if (self::$verbosity > 2) { 137 | self::write('INFO', self::exceptionmessage($e)); 138 | } 139 | } 140 | 141 | /** 142 | * Log an exception as a debug. 143 | */ 144 | public static function debugex(Throwable $e) 145 | { 146 | if (self::$verbosity > 3) { 147 | self::write('DEBUG', self::exceptionmessage($e)); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /app/class/Matchable.php: -------------------------------------------------------------------------------- 1 | path = "/" . rtrim(Model::MEDIA_DIR, "/"); 28 | $this->type = Media::mediatypes(); 29 | $this->hydrate($datas); 30 | } 31 | 32 | /** 33 | * Generate link address for table header 34 | * 35 | * @param string $sortby 36 | * @return string link address 37 | */ 38 | public function getsortbyaddress(string $sortby): string 39 | { 40 | if (!in_array($sortby, Modelmedia::MEDIA_SORTBY)) { 41 | $sortby = 'id'; 42 | } 43 | if ($this->sortby === $sortby) { 44 | $order = $this->order * -1; 45 | } else { 46 | $order = $this->order; 47 | } 48 | $query = ['path' => $this->path, 'sortby' => $sortby, 'order' => $order]; 49 | if (array_diff(Media::mediatypes(), $this->type) != []) { 50 | $query['type'] = $this->type; 51 | } 52 | return '?' . urldecode(http_build_query($query)); 53 | } 54 | 55 | /** 56 | * Give the GET params to be used for redirection. Using hidden input under the `route` name. 57 | * 58 | * @param string $path Media path to display. Default is the current path. 59 | * @return string URL-encoded path, filter and sort parameters, startiting with a `?` 60 | */ 61 | public function getpathaddress(string $path = null): string 62 | { 63 | $path = is_null($path) ? $this->path : "/$path"; 64 | $query = ['path' => $path, 'sortby' => $this->sortby, 'order' => $this->order]; 65 | if (array_diff(Media::mediatypes(), $this->type) != []) { 66 | $query['type'] = $this->type; 67 | } 68 | return '?' . urldecode(http_build_query($query)); 69 | } 70 | 71 | 72 | // ___________________ MAGIC FOLDERS _____________________ 73 | 74 | 75 | public function isfontdir(): bool 76 | { 77 | return $this->dir() === Model::FONT_DIR; 78 | } 79 | 80 | public function iscssdir(): bool 81 | { 82 | return $this->dir() === Model::CSS_DIR; 83 | } 84 | 85 | public function isthumbnaildir(): bool 86 | { 87 | return $this->dir() === Model::THUMBNAIL_DIR; 88 | } 89 | 90 | public function isfavicondir(): bool 91 | { 92 | return $this->dir() === Model::FAVICON_DIR; 93 | } 94 | 95 | // ______________________________________________ G E T ________________________________________________________ 96 | 97 | 98 | /** 99 | * @return string formated like `/media/` 100 | */ 101 | public function path() 102 | { 103 | return $this->path; 104 | } 105 | 106 | /** 107 | * @return string formated like `media//` 108 | */ 109 | public function dir() 110 | { 111 | return trim($this->path, '/') . '/'; 112 | } 113 | 114 | public function sortby() 115 | { 116 | return $this->sortby; 117 | } 118 | 119 | public function order() 120 | { 121 | return $this->order; 122 | } 123 | 124 | public function type() 125 | { 126 | return $this->type; 127 | } 128 | 129 | // ______________________________________________ S E T ________________________________________________________ 130 | 131 | 132 | /** 133 | * @param string $path 134 | */ 135 | public function setpath(string $path) 136 | { 137 | // gather nested slashs 138 | $path = preg_replace("%\/{2,}%", "/", $path); 139 | $this->path = "/" . trim($path, "/"); 140 | } 141 | 142 | public function setsortby(string $sortby) 143 | { 144 | if (in_array($sortby, Modelmedia::MEDIA_SORTBY)) { 145 | $this->sortby = $sortby; 146 | } 147 | } 148 | 149 | public function setorder(int $order) 150 | { 151 | if ($order === -1 || $order === 1) { 152 | $this->order = $order; 153 | } 154 | } 155 | 156 | public function settype($type) 157 | { 158 | if (is_array($type)) { 159 | $this->type = array_intersect(Media::mediatypes(), array_unique($type)); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /app/class/Mediaoptlist.php: -------------------------------------------------------------------------------- 1 | options), $datas); 26 | $this->hydrate($datas); 27 | } 28 | 29 | /** 30 | * Generate HTML displaying list of medias 31 | * 32 | * @throws RuntimeException If something went wrong 33 | */ 34 | public function generatecontent(): string 35 | { 36 | $mediamanager = new Modelmedia(); 37 | $medialist = $mediamanager->medialistopt($this); 38 | 39 | $dirid = str_replace('/', '-', $this->path); 40 | 41 | $div = "
\n"; 42 | 43 | foreach ($medialist as $media) { 44 | $div .= '
'; 45 | $id = 'id="media_' . $media->filename() . '"'; 46 | $path = $media->getincludepath(); 47 | $ext = $media->extension(); 48 | $filename = $media->filename(); 49 | if ($media->type() == 'image') { 50 | $div .= '' . $media->filename() . ''; 51 | } elseif ($media->type() == 'sound') { 52 | $div .= '
\n"; 63 | } 64 | 65 | $div .= "
\n"; 66 | 67 | return $div; 68 | } 69 | 70 | public function getquery() 71 | { 72 | $query = [ 73 | 'path' => $this->path, 74 | 'sortby' => $this->sortby, 75 | 'order' => $this->order, 76 | 'filename' => $this->filename 77 | ]; 78 | if (array_diff(Media::mediatypes(), $this->type) !== []) { 79 | $query['type'] = $this->type; 80 | } 81 | return urldecode(http_build_query($query)); 82 | } 83 | 84 | /** 85 | * Get the code to insert directly 86 | */ 87 | public function getcode(): string 88 | { 89 | return '%MEDIA?' . $this->getquery() . '%'; 90 | } 91 | 92 | public function getaddress(): string 93 | { 94 | return '?' . $this->getquery(); 95 | } 96 | 97 | 98 | // ______________________________________________ G E T ________________________________________________________ 99 | 100 | 101 | public function fullmatch(): string 102 | { 103 | return $this->fullmatch; 104 | } 105 | 106 | public function options(): string 107 | { 108 | return $this->options; 109 | } 110 | 111 | public function filename(): int 112 | { 113 | return $this->filename; 114 | } 115 | 116 | // ______________________________________________ S E T ________________________________________________________ 117 | 118 | 119 | public function setfullmatch(string $fullmatch) 120 | { 121 | $this->fullmatch = $fullmatch; 122 | } 123 | 124 | 125 | public function setoptions(string $options) 126 | { 127 | if (!empty($options)) { 128 | $this->options = $options; 129 | } 130 | } 131 | 132 | public function setfilename($filename) 133 | { 134 | $this->filename = (int) (bool) $filename; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/class/Modeladmin.php: -------------------------------------------------------------------------------- 1 | selected = true; 27 | } 28 | $folders[] = $folder; 29 | } 30 | return $folders; 31 | } catch (RuntimeException $e) { 32 | $m = $e->getMessage(); 33 | throw new RuntimeException("Error when trying to list page tables: $m"); 34 | } 35 | } 36 | 37 | /** 38 | * Duplicate current page database using new name 39 | * 40 | * @param string $name of the new database 41 | */ 42 | public function duplicate(string $name): void 43 | { 44 | $this->copydb(Config::pagetable(), $name); 45 | } 46 | 47 | /** 48 | * Copy database folder to a new folder if it doeas not already exsit 49 | * 50 | * @param string $db name of source page database to copy 51 | * @param string $name of the destination database 52 | */ 53 | public function copydb(string $db, string $name): void 54 | { 55 | $dbdir = self::PAGES_DIR . $db; 56 | $newdbdir = self::PAGES_DIR . Model::idclean($name); 57 | if (is_dir($dbdir) && !is_dir($newdbdir)) { 58 | Fs::recursecopy($dbdir, $newdbdir); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/class/Modelconnect.php: -------------------------------------------------------------------------------- 1 | $userid, 21 | "wsession" => $wsession 22 | ]; 23 | if (empty(Config::secretkey())) { 24 | throw new RuntimeException("Secret Key not set"); 25 | } 26 | $jwt = JWT::encode($datas, Config::secretkey()); 27 | $options = [ 28 | 'expires' => time() + $conservation * 24 * 3600, 29 | 'path' => '/' . Config::basepath(), 30 | 'domain' => '', 31 | 'secure' => Config::issecure(), 32 | 'httponly' => true, 33 | 'samesite' => 'Strict' 34 | ]; 35 | $cookie = setcookie('rememberme', $jwt, $options); 36 | if (!$cookie) { 37 | throw new RuntimeException("Remember me cookie cannot be created"); 38 | } 39 | } 40 | 41 | /** 42 | * Get decoded cookie using JWT 43 | * @return string[] Associative array containing JWT token's datas 44 | * @throws RuntimeException If JWT token decode failed or auth cookie is unset 45 | */ 46 | public function checkcookie(): array 47 | { 48 | if (!empty($_COOKIE['rememberme'])) { 49 | $datas = JWT::decode($_COOKIE['rememberme'], Config::secretkey(), ['HS256']); 50 | return get_object_vars($datas); 51 | } else { 52 | throw new RuntimeException('Auth cookie is unset'); 53 | } 54 | } 55 | 56 | /** 57 | * Delete authentication cookie 58 | */ 59 | public function deleteauthcookie(): void 60 | { 61 | $_COOKIE['rememberme'] = []; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/class/Modeldb.php: -------------------------------------------------------------------------------- 1 | dbinit(); 30 | } 31 | 32 | /** 33 | * Check if database directory have at least the minimal free disk space required left. 34 | * 35 | * @return bool True if enought space left, otherwise False 36 | */ 37 | protected function isdiskfree(): bool 38 | { 39 | try { 40 | return (disk_free_space_ex(self::DATABASE_DIR) > self::MINIMAL_DISK_SPACE); 41 | } catch (RuntimeException $e) { 42 | throw new InvalidArgumentException($e->getMessage()); 43 | } 44 | } 45 | 46 | /** 47 | * Store Document but only if there is enough space left on disk 48 | * 49 | * @param Document $document Flywheel Document 50 | * @return bool True in case of success, otherwise false 51 | * 52 | * @todo use exceptions to create a disctinction between differents possible problems 53 | */ 54 | protected function storedoc(DocumentInterface $document): bool 55 | { 56 | if (!$this->isdiskfree()) { 57 | Logger::error("Not enough free space on disk to store datas in database"); 58 | return false; 59 | } 60 | return $this->repo->store($document); 61 | } 62 | 63 | /** 64 | * Update Document but only if there is enough space left on disk 65 | * 66 | * @param Document $document Flywheel Document 67 | * @return bool True in case of success, otherwise false 68 | * 69 | * @todo use exceptions to create a disctinction between differents possible problems 70 | */ 71 | protected function updatedoc(DocumentInterface $document): bool 72 | { 73 | if (!$this->isdiskfree()) { 74 | Logger::error("Not enough free space on disk to update datas in database"); 75 | return false; 76 | } 77 | return $this->repo->update($document); 78 | } 79 | 80 | /** 81 | * Init database config 82 | * 83 | * @param string $dir Directory where repo is stored. 84 | */ 85 | protected function dbinit(string $dir = Model::DATABASE_DIR): void 86 | { 87 | $this->database = new Config($dir, [ 88 | 'query_class' => Query::class, 89 | 'formatter' => new JSON(), 90 | ]); 91 | } 92 | 93 | /** 94 | * Init store. 95 | * 96 | * @param string $repo Name of the repo 97 | * 98 | * @throws LogicException if this failed 99 | */ 100 | protected function storeinit(string $repo): void 101 | { 102 | try { 103 | $this->repo = new Repository($repo, $this->database); 104 | } catch (RuntimeException $e) { 105 | throw new LogicException($e->getMessage()); 106 | } 107 | } 108 | 109 | /** 110 | * List every IDs of a database 111 | * 112 | * @return string[] array of ID strings 113 | */ 114 | public function list(): array 115 | { 116 | return $this->repo->getAllIds(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/class/Modelldap.php: -------------------------------------------------------------------------------- 1 | ldapserver = $ldapserver; 35 | $this->connection = @ldap_connect($this->ldapserver); 36 | if ($this->connection === false) { 37 | throw new RuntimeException('bad LDAP server syntax'); 38 | } 39 | $this->tree = $tree; 40 | $this->u = $u; 41 | ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, 3); 42 | } 43 | 44 | /** 45 | * Try to authenticate user against CLUB1 local LDAP server 46 | * 47 | * @param string $username 48 | * @param string $password 49 | * 50 | * @return bool indicating if auth is a success 51 | * 52 | * @throws RuntimeException If LDAP connection failed 53 | */ 54 | public function auth(string $username, string $password): bool 55 | { 56 | $binddn = "$this->u=$username,$this->tree"; 57 | 58 | $ldapbind = @ldap_bind($this->connection, $binddn, $password); 59 | if ($ldapbind === false) { 60 | $errno = ldap_errno($this->connection); 61 | switch ($errno) { 62 | case self::LDAP_INVALID_CREDENTIALS: 63 | return false; 64 | } 65 | throw new RuntimeException(ldap_err2str($errno)); 66 | } 67 | return true; 68 | } 69 | 70 | public function disconnect(): void 71 | { 72 | ldap_close($this->connection); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/class/Optcode.php: -------------------------------------------------------------------------------- 1 | id(), $encoded); 20 | parse_str(ltrim($encoded, "?"), $datas); 21 | $this->hydrate($datas); 22 | } 23 | 24 | public function bookmark() 25 | { 26 | return $this->bookmark; 27 | } 28 | 29 | /** 30 | * @param string $bookmark Bookmark ID 31 | */ 32 | public function setbookmark(string $bookmark) 33 | { 34 | if (Model::idcheck($bookmark)) { 35 | $this->bookmark = $bookmark; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/class/Optmap.php: -------------------------------------------------------------------------------- 1 | getquery() . '%'; 17 | } 18 | 19 | /** 20 | * Generate the HTML code for the map 21 | */ 22 | public function maphtml(array $pages, AltoRouter $router): string 23 | { 24 | $geopages = array_map(function (Page $page) use ($router) { 25 | $data = $page->drylist(['id', 'title', 'latitude', 'longitude']); 26 | try { 27 | $data['read'] = $router->generate('pageread', ['page' => $page->id()]); 28 | } catch (Exception $e) { 29 | throw new LogicException($e->getMessage()); 30 | } 31 | return $data; 32 | }, $pages); 33 | $geopages = array_values($geopages); 34 | $json = json_encode($geopages); 35 | $mapid = 'geomap'; 36 | 37 | $html = "
\n"; 38 | 39 | $html .= ""; 40 | 41 | 42 | 43 | return $html; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/class/Optrandom.php: -------------------------------------------------------------------------------- 1 | getquery() . '%'; 16 | } 17 | 18 | public function origin(): string 19 | { 20 | return $this->origin; 21 | } 22 | 23 | public function setorigin($origin) 24 | { 25 | if (Model::idcheck($origin)) { 26 | $this->origin = $origin; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/class/Pagev1.php: -------------------------------------------------------------------------------- 1 | setheader(''); 23 | $this->setmain(''); 24 | $this->setnav(''); 25 | $this->setaside(''); 26 | $this->setfooter(''); 27 | 28 | $this->setinterface('main'); 29 | } 30 | 31 | 32 | public function header($type = 'string') 33 | { 34 | return $this->header; 35 | } 36 | 37 | public function main($type = 'string') 38 | { 39 | return $this->main; 40 | } 41 | 42 | public function primary($type = ''): string 43 | { 44 | return $this->main; 45 | } 46 | 47 | public function nav($type = "string") 48 | { 49 | return $this->nav; 50 | } 51 | 52 | public function aside($type = "string") 53 | { 54 | return $this->aside; 55 | } 56 | 57 | public function footer($type = "string") 58 | { 59 | return $this->footer; 60 | } 61 | 62 | 63 | public function setheader($header) 64 | { 65 | if (strlen($header) < self::LENGTH_LONG_TEXT && is_string($header)) { 66 | $header = crlf2lf($header); 67 | $this->header = $header; 68 | } 69 | } 70 | 71 | public function setmain($main) 72 | { 73 | if (strlen($main) < self::LENGTH_LONG_TEXT and is_string($main)) { 74 | $main = crlf2lf($main); 75 | $this->main = $main; 76 | } 77 | } 78 | 79 | public function setnav($nav) 80 | { 81 | if (strlen($nav) < self::LENGTH_LONG_TEXT and is_string($nav)) { 82 | $nav = crlf2lf($nav); 83 | $this->nav = $nav; 84 | } 85 | } 86 | 87 | public function setaside($aside) 88 | { 89 | if (strlen($aside) < self::LENGTH_LONG_TEXT and is_string($aside)) { 90 | $aside = crlf2lf($aside); 91 | $this->aside = $aside; 92 | } 93 | } 94 | 95 | public function setfooter($footer) 96 | { 97 | if (strlen($footer) < self::LENGTH_LONG_TEXT and is_string($footer)) { 98 | $footer = crlf2lf($footer); 99 | $this->footer = $footer; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/class/Pagev2.php: -------------------------------------------------------------------------------- 1 | setcontent(''); 18 | 19 | $this->setinterface('content'); 20 | } 21 | 22 | public function content($type = ''): string 23 | { 24 | return $this->content; 25 | } 26 | 27 | public function primary($type = ''): string 28 | { 29 | return $this->content; 30 | } 31 | 32 | public function setcontent($content) 33 | { 34 | if (is_string($content)) { 35 | $this->content = $content; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/class/Route.php: -------------------------------------------------------------------------------- 1 | hydrate($vars); 17 | } 18 | 19 | public function toarray() 20 | { 21 | $array = []; 22 | if (!empty($this->id)) { 23 | $array[] = 'page'; 24 | } 25 | if (!empty($this->aff)) { 26 | $array[] = 'aff=' . $this->aff; 27 | } 28 | if (!empty($this->action)) { 29 | $array[] = 'action=' . $this->action; 30 | } 31 | if (!empty($this->redirect)) { 32 | $array[] = $this->redirect; 33 | } 34 | 35 | 36 | return $array; 37 | } 38 | 39 | public function tostring() 40 | { 41 | return implode(' ', $this->toarray()); 42 | } 43 | 44 | 45 | 46 | public function setid($id) 47 | { 48 | $this->id = $id; 49 | } 50 | 51 | public function setaff($aff) 52 | { 53 | $this->aff = $aff; 54 | } 55 | 56 | public function setaction($action) 57 | { 58 | $this->action = $action; 59 | } 60 | 61 | public function setredirect($redirect) 62 | { 63 | $this->redirect = $redirect; 64 | } 65 | 66 | public function id() 67 | { 68 | return $this->id; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/class/Servicefont.php: -------------------------------------------------------------------------------- 1 | mediamanager = $mediamanager; 23 | $mediaopt = new Mediaopt(['path' => Model::FONT_DIR, 'type' => [Media::FONT]]); 24 | $medias = $this->mediamanager->medialistopt($mediaopt); 25 | $this->fonts = $this->groupfonts($medias); 26 | } 27 | 28 | /** 29 | * This will group the media by filename 30 | * 31 | * @param Media[] $medias 32 | * 33 | * @return Font[] 34 | */ 35 | protected function groupfonts(array $medias): array 36 | { 37 | $groupedmedias = []; 38 | foreach ($medias as $media) { 39 | $groupedmedias[$media->getbasefilename()][] = $media; 40 | } 41 | $fonts = []; 42 | foreach ($groupedmedias as $medias) { 43 | $fonts[] = new Font($medias); 44 | } 45 | return $fonts; 46 | } 47 | 48 | /** 49 | * Generate CSS file with @fontface rules according to font folder 50 | * 51 | * @throws Filesystemexception 52 | */ 53 | public function writecss(): void 54 | { 55 | $fontcss = $this->css(); 56 | Fs::writefile(Model::FONTS_CSS_FILE, $fontcss, 0664); 57 | } 58 | 59 | /** 60 | * Generate CSS file as a string using stored fonts 61 | * 62 | * @return string CSS file ready to be written somewhere 63 | */ 64 | protected function css(): string 65 | { 66 | $css = ""; 67 | foreach ($this->fonts as $font) { 68 | $css .= $font->fontface(); 69 | } 70 | return $css; 71 | } 72 | 73 | /** 74 | * @return Font[] List of Fonts objects 75 | */ 76 | public function fonts(): array 77 | { 78 | return $this->fonts; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/class/Servicepostprocess.php: -------------------------------------------------------------------------------- 1 | page = $page; 34 | $this->user = $user; 35 | $this->action = $page->postprocessaction(); 36 | } 37 | 38 | /** 39 | * Apply post process to page HTML render 40 | */ 41 | public function process(string $html): string 42 | { 43 | $html = $this->jsvars($html); 44 | if ($this->action) { 45 | $html = $this->replace($html); 46 | } 47 | return $html; 48 | } 49 | 50 | /** 51 | * Inject Javscript vars inside HTML head of the page 52 | */ 53 | private function jsvars(string $html): string 54 | { 55 | try { 56 | $wobj = $this->wobj($this->page, $this->user); 57 | } catch (JsonException $e) { 58 | $wobj = '{}'; 59 | } 60 | $script = "\n"; 61 | return insert_after($html, '', $script); 62 | } 63 | 64 | /** 65 | * Replace counters by their values 66 | */ 67 | private function replace(string $text): string 68 | { 69 | $visitcount = $this->page->visitcount(); 70 | $editcount = $this->page->editcount(); 71 | $displaycount = $this->page->displaycount(); 72 | 73 | $replacements = [ 74 | self::VISIT_COUNT => "$visitcount", 75 | self::EDIT_COUNT => "$editcount", 76 | self::AFF_COUNT => "$displaycount", 77 | ]; 78 | return strtr($text, $replacements); 79 | } 80 | 81 | /** 82 | * @return string JSON encoded w global, pages and user datas 83 | * @throws JsonException If JSON encoding failed 84 | */ 85 | private function wobj(Page $page, User $user): string 86 | { 87 | $wdatas = [ 88 | 'page' => [ 89 | 'id' => $page->id(), 90 | 'title' => $page->title(), 91 | 'description' => $page->description(), 92 | 'secure' => $page->secure(), 93 | ], 94 | 'domain' => Config::url(), 95 | 'basepath' => Config::basepath(), 96 | 'user' => [ 97 | 'id' => $user->id(), 98 | 'level' => $user->level(), 99 | 'name' => $user->name(), 100 | ] 101 | ]; 102 | return json_encode($wdatas, JSON_THROW_ON_ERROR); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/class/Servicerenderv2.php: -------------------------------------------------------------------------------- 1 | page = $page; 34 | $html = $this->bodyconstructor('%CONTENT%'); 35 | return $this->bodyparser($html); 36 | } 37 | 38 | 39 | /** 40 | * Analyse BODY, call the corresponding CONTENTs and render everything 41 | * 42 | * @param string $body as the string BODY of the page 43 | * 44 | * @return string as the full rendered BODY of the page 45 | */ 46 | protected function bodyconstructor(string $body): string 47 | { 48 | $body = parent::bodyconstructor($body); 49 | 50 | $matches = $this->match($body, 'CONTENT'); 51 | 52 | // First, analyse the synthax and call the corresponding methods 53 | if (!empty($matches)) { 54 | foreach ($matches as $match) { 55 | $element = new Elementv2($this->page->id(), $match['fullmatch'], $match['options']); 56 | $element->setcontent($this->getelementcontent($element->id())); 57 | $element->setcontent($this->elementparser($element)); 58 | $body = str_replace($element->fullmatch(), $element->content(), $body); 59 | } 60 | } 61 | 62 | return $body; 63 | } 64 | 65 | protected function elementparser(Elementv2 $element) 66 | { 67 | $content = $element->content(); 68 | $content = $this->winclusions($content); 69 | if ($element->everylink() > 0) { 70 | $content = $this->everylink($content, $element->everylink()); 71 | } 72 | if ($element->markdown()) { 73 | $content = $this->markdown($content); 74 | } 75 | if ($element->headerid()) { 76 | $content = $this->headerid( 77 | $content, 78 | $element->minheaderid(), 79 | $element->maxheaderid(), 80 | $element->headeranchor(), 81 | ); 82 | } 83 | if ($element->urllinker()) { 84 | $content = $this->autourl($content); 85 | } 86 | 87 | return $content; 88 | } 89 | 90 | 91 | /** 92 | * Get element content by looking for source page ID. 93 | * If ID is not used: return empty string 94 | * If source page is V1, it will use the MAIN content. 95 | * 96 | * @param string $source Source Page ID 97 | * 98 | * @return string Source Page primary content or empty string 99 | * 100 | * @todo Log errors somewhere 101 | */ 102 | protected function getelementcontent(string $source): string 103 | { 104 | if ($source === $this->page->id()) { 105 | return $this->page->content(); 106 | } else { 107 | try { 108 | $page = $this->pagemanager->get($source); 109 | return $page->primary(); 110 | } catch (RuntimeException $e) { 111 | // page ID is not used 112 | return ''; 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/class/Servicesession.php: -------------------------------------------------------------------------------- 1 | dry(); 50 | } 51 | 52 | public function getworkspace(): Workspace 53 | { 54 | $datas = $_SESSION['workspace'] ?? []; 55 | return new Workspace($datas); 56 | } 57 | 58 | public function setgraph(Graph $graph): void 59 | { 60 | $_SESSION['graph'] = $graph->dry(); 61 | } 62 | 63 | public function getgraph(): Graph 64 | { 65 | $datas = $_SESSION['graph'] ?? []; 66 | return new Graph($datas); 67 | } 68 | 69 | /** 70 | * Empty current user session 71 | */ 72 | public function empty(): void 73 | { 74 | $_SESSION = []; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/class/Servicetags.php: -------------------------------------------------------------------------------- 1 | $count) { 54 | $bgcolor = new Color(mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255)); 55 | $txtcolor = $bgcolor->luma() > 128 ? 'black' : 'white'; 56 | $tagdata[$tag] = [ 57 | 'background-color' => $bgcolor->hexa(), 58 | 'color' => $txtcolor 59 | ]; 60 | } 61 | Fs::writefile(Model::TAGS_FILE, json_encode($tagdata, JSON_PRETTY_PRINT)); 62 | $this->generatecssfile($tagdata); 63 | return $tagdata; 64 | } 65 | 66 | /** 67 | * update tag colors 68 | * This also re-generate the CSS file. 69 | * 70 | * @throws Filesystemexception When error occured with writing files 71 | */ 72 | public function updatecolors($post): void 73 | { 74 | $tagdata = []; 75 | foreach ($post as $tag => $color) { 76 | $color = sscanf($color, "#%02x%02x%02x"); 77 | $bgcolor = new Color($color[0], $color[1], $color[2]); 78 | $txtcolor = $bgcolor->luma() > 128 ? 'black' : 'white'; 79 | $tagdata[$tag] = [ 80 | 'background-color' => $bgcolor->hexa(), 81 | 'color' => $txtcolor 82 | ]; 83 | } 84 | Fs::writefile(Model::TAGS_FILE, json_encode($tagdata, JSON_PRETTY_PRINT)); 85 | $this->generatecssfile($tagdata); 86 | } 87 | 88 | /** 89 | * Generate CSS file according to tag datas 90 | * 91 | * @param array $tagdata Array of tags as keys and two sub-arrays: background-color and color 92 | * 93 | * @throws Filesystemexception If an error while saving CSS file 94 | */ 95 | protected function generatecssfile(array $tagdata): void 96 | { 97 | $css = ''; 98 | foreach ($tagdata as $tag => $datas) { 99 | $bgcolor = $datas['background-color']; 100 | $color = $datas['color']; 101 | $css .= "\n.tag_$tag { background-color: $bgcolor; color: $color; }"; 102 | } 103 | Fs::writefile(Model::COLORS_FILE, $css, 0664); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/class/Summary.php: -------------------------------------------------------------------------------- 1 | hydrate($datas); 32 | $this->readoptions(); 33 | } 34 | 35 | 36 | protected function readoptions(): void 37 | { 38 | parse_str(htmlspecialchars_decode($this->options), $datas); 39 | $this->hydrate($datas); 40 | } 41 | 42 | 43 | /** 44 | * Generate a Summary based on header ids. Need to use `$this->headerid` before to scan text 45 | * 46 | * @return string html list with anchor link 47 | */ 48 | public function sumparser() 49 | { 50 | // check if a element is specified 51 | if (!is_null($this->element) && isset($this->sum[$this->element])) { 52 | $headers = $this->sum[$this->element()]; 53 | } else { 54 | $headers = flatten($this->sum); 55 | } 56 | 57 | $sumstring = ''; 58 | $minlevel = $this->min - 1; 59 | $prevlevel = $minlevel; 60 | 61 | foreach ($headers as $header) { 62 | if ($header->level < $this->min || $header->level > $this->max) { 63 | // not in the accepted range, skiping this header. 64 | continue; 65 | }; 66 | for ($i = $header->level; $i > $prevlevel; $i--) { 67 | $class = $i === $this->min ? ' class="summary"' : ''; 68 | $sumstring .= "
  • "; 69 | } 70 | for ($i = $header->level; $i < $prevlevel; $i++) { 71 | $sumstring .= '
  • '; 72 | } 73 | if ($header->level <= $prevlevel) { 74 | $sumstring .= '
  • '; 75 | } 76 | $sumstring .= "id\">$header->title"; 77 | $prevlevel = $header->level; 78 | } 79 | for ($i = $minlevel; $i < $prevlevel; $i++) { 80 | $sumstring .= "
  • "; 81 | } 82 | return $sumstring; 83 | } 84 | 85 | 86 | 87 | // ________________________________________________ G E T ________________________________________________________ 88 | 89 | 90 | public function fullmatch(): string 91 | { 92 | return $this->fullmatch; 93 | } 94 | 95 | public function options(): string 96 | { 97 | return $this->options; 98 | } 99 | 100 | public function element() 101 | { 102 | return $this->element; 103 | } 104 | 105 | 106 | // ________________________________________________ S E T ________________________________________________________ 107 | 108 | 109 | public function setfullmatch(string $fullmatch): void 110 | { 111 | $this->fullmatch = $fullmatch; 112 | } 113 | 114 | 115 | public function setoptions(string $options): void 116 | { 117 | if (!empty($options)) { 118 | $this->options = $options; 119 | } 120 | } 121 | 122 | public function setmin($min) 123 | { 124 | $min = intval($min); 125 | if ($min >= 1 && $min <= 6) { 126 | $this->min = $min; 127 | } 128 | } 129 | 130 | public function setmax($max) 131 | { 132 | $max = intval($max); 133 | if ($max >= 1 && $max <= 6) { 134 | $this->max = $max; 135 | } 136 | } 137 | 138 | public function setsum(array $sum) 139 | { 140 | $this->sum = $sum; 141 | } 142 | 143 | public function setelement(string $element) 144 | { 145 | if (in_array($element, Pagev1::HTML_ELEMENTS)) { 146 | $this->element = $element; 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /app/class/Workspace.php: -------------------------------------------------------------------------------- 1 | hydrate($datas); 36 | } 37 | 38 | public function showeditorleftpanel(): bool 39 | { 40 | return $this->showeditorleftpanel; 41 | } 42 | 43 | public function showeditorrightpanel(): bool 44 | { 45 | return $this->showeditorrightpanel; 46 | } 47 | 48 | public function showhomeoptionspanel(): bool 49 | { 50 | return $this->showhomeoptionspanel; 51 | } 52 | 53 | public function showhomebookmarkspanel(): bool 54 | { 55 | return $this->showhomebookmarkspanel; 56 | } 57 | 58 | public function showmediaoptionspanel(): bool 59 | { 60 | return $this->showmediaoptionspanel; 61 | } 62 | 63 | public function showmediatreepanel(): bool 64 | { 65 | return $this->showmediatreepanel; 66 | } 67 | 68 | public function fontsize(): int 69 | { 70 | return $this->fontsize; 71 | } 72 | 73 | public function mediadisplay(): string 74 | { 75 | return $this->mediadisplay; 76 | } 77 | 78 | public function highlighttheme(): string 79 | { 80 | return $this->highlighttheme; 81 | } 82 | 83 | public function setshoweditorleftpanel($show): void 84 | { 85 | $this->showeditorleftpanel = boolval($show); 86 | } 87 | 88 | public function setshoweditorrightpanel($show): void 89 | { 90 | $this->showeditorrightpanel = boolval($show); 91 | } 92 | 93 | public function setshowhomeoptionspanel($show): void 94 | { 95 | $this->showhomeoptionspanel = boolval($show); 96 | } 97 | 98 | public function setshowhomebookmarkspanel($show): void 99 | { 100 | $this->showhomebookmarkspanel = boolval($show); 101 | } 102 | 103 | public function setshowmediaoptionspanel($show): void 104 | { 105 | $this->showmediaoptionspanel = boolval($show); 106 | } 107 | 108 | public function setshowmediatreepanel($show): void 109 | { 110 | $this->showmediatreepanel = boolval($show); 111 | } 112 | 113 | public function setfontsize($fontsize): void 114 | { 115 | $fontsize = intval($fontsize); 116 | if ($fontsize >= self::FONTSIZE_MIN && $fontsize <= self::FONTSIZE_MAX) { 117 | $this->fontsize = $fontsize; 118 | } 119 | } 120 | 121 | public function setmediadisplay(string $mediadisplay): void 122 | { 123 | if (in_array($mediadisplay, self::MEDIA_DISPLAY)) { 124 | $this->mediadisplay = $mediadisplay; 125 | } 126 | } 127 | 128 | public function sethighlighttheme(string $theme): void 129 | { 130 | if (in_array($theme, self::THEMES)) { 131 | $this->highlighttheme = $theme; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /app/view/templates/alertcommandnotfound.php: -------------------------------------------------------------------------------- 1 | layout('alertlayout') ?> 2 | 3 | start('alert') ?> 4 | 5 |

    6 | 🛑 Command /e($command) ?> not found 7 |

    8 | 9 |

    👁️ read page e($id) ?>

    10 | 11 | isvisitor()) : ?> 12 |

    13 | 💡 You may want to try: 14 |

      15 |
    • 16 | /add to create a new page 17 |
    • 18 |
    • 19 | /edit to edit the page 20 |
    • 21 |
    • 22 | /render to render the page 23 |
    • 24 |
    • 25 | /delete to delete the page 26 |
    • 27 |
    • 28 | /download to get JSON file of the page 29 |
    • 30 |
    31 |

    32 | 33 | 34 | stop() ?> 35 | 36 | -------------------------------------------------------------------------------- /app/view/templates/alertexistnot.php: -------------------------------------------------------------------------------- 1 | layout('alertlayout', ['subtitle' => $subtitle]) ?> 2 | 3 | start('alert') ?> 4 | 5 | isvisitor() && Wcms\Config::existnotpass()) : ?> 6 | 7 |

    8 | insert('alertform', ['id' => $page->id()]) ?> 9 |

    10 | 11 | 12 | 13 | 14 | 15 | iseditor()) : ?> 16 |

    17 | ⭐ Create 18 |

    19 | 20 |

    21 | 💡 To create a page in one command, you can type 22 | upage('pageadd', $page->id()) ?> 23 | directly in your address bar. 24 |

    25 | 26 |

    27 | 🏠 Go back to home 28 |

    29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | stop() ?> 39 | 40 | -------------------------------------------------------------------------------- /app/view/templates/alertform.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 | -------------------------------------------------------------------------------- /app/view/templates/alertlayout.php: -------------------------------------------------------------------------------- 1 | layout('readerlayout') ?> 2 | 3 | start('head') ?> 4 | 5 | ' : '' ?> 6 | 7 | 8 | 9 | 10 | stop() ?> 11 | 12 | 13 | 14 | start('page') ?> 15 | 16 | 29 | 30 | 31 |
    32 | ' . Wcms\Config::alerttitle() . '' : '' ?> 33 | 34 | 35 |

    36 | 37 |

    38 | 39 | 40 | section('alert')?> 41 | 42 | 43 | 44 |

    45 | 46 | 47 | 48 |

    49 | 50 | 51 |
    52 | 53 | 54 | 55 | stop() ?> 56 | -------------------------------------------------------------------------------- /app/view/templates/alertnotpublished.php: -------------------------------------------------------------------------------- 1 | layout('alertlayout', ['subtitle' => $subtitle]) ?> 2 | 3 | start('alert') ?> 4 | 5 | 6 | isvisitor() && Wcms\Config::notpublishedpass()) : ?> 7 | 8 |

    9 | insert('alertform', ['id' => $page->id()]) ?> 10 |

    11 | 12 | 13 | 14 | stop() ?> 15 | 16 | -------------------------------------------------------------------------------- /app/view/templates/alertprivate.php: -------------------------------------------------------------------------------- 1 | layout('alertlayout', ['subtitle' => $subtitle]) ?> 2 | 3 | start('alert') ?> 4 | 5 | 6 | isvisitor() && Wcms\Config::privatepass()) : ?> 7 | 8 |

    9 | insert('alertform', ['id' => $page->id()]) ?> 10 |

    11 | 12 | 13 | 14 | 15 | 16 | 17 | stop() ?> 18 | 19 | -------------------------------------------------------------------------------- /app/view/templates/alertrandom.php: -------------------------------------------------------------------------------- 1 | layout('alertlayout', ['subtitle' => '']) ?> 2 | 3 | 4 | 5 | 6 | start('alert') ?> 7 | 8 | 9 | ' . Wcms\Config::alerttitle() . '' : '' ?> 10 | 11 |

    Random page tool error

    12 | 13 |

    14 | 15 | stop() ?> 16 | -------------------------------------------------------------------------------- /app/view/templates/backtopbar.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | 16 | 17 | iseditor()) : ?> 18 | > 19 | home 20 | 21 | > 22 | media 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
    33 | 34 |
    35 | isadmin()) : ?> 36 | > 37 | users 38 | 39 | > 40 | admin 41 | 42 | 43 | 44 | > 45 | documentation 46 | 47 | 48 | > 49 | id() ?> 50 | 51 | 52 |
    53 | 54 | 55 | 56 | 57 | 58 |
    59 |
    60 | 61 |
    62 | -------------------------------------------------------------------------------- /app/view/templates/connect.php: -------------------------------------------------------------------------------- 1 | layout('layout', ['title' => 'Connect', 'description' => 'connect', 'stylesheets' => [$css . 'back.css', $css . 'connect.css']]) ?> 2 | 3 | 4 | start('page') ?> 5 | 6 |
    7 |

    Login

    8 | 9 | isvisitor()) : ?> 10 | 11 |
    12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
    25 | 26 | 27 | 28 |
    29 | 30 |
    31 | 32 | 33 | 34 | 35 |

    back to page read view

    36 | 37 |
    38 | 39 | stop() ?> 40 | -------------------------------------------------------------------------------- /app/view/templates/delete.php: -------------------------------------------------------------------------------- 1 | id() ?> 2 | layout('modallayout', ['title' => "Delete page $id", 'description' => 'delete', 'css' => $css, 'user' => $user, 'pagelist' => $pagelist]) ?> 3 | 4 | start('modal') ?> 5 | 6 |

    URL : upage('pageread', $page->id()) ?>

    7 |

    Id : id() ?>

    8 |

    Title : e($page->title()) ?>

    9 |

    Number of edits : editcount() ?>

    10 |

    Number of displays : displaycount() ?>

    11 |

    12 | Page linking to this one : 13 | 0) : ?> 14 | 15 | 16 | 17 | 18 |

    19 | 20 |
    21 | 22 | 26 | 27 | 28 | 29 | Cancel 30 | 31 | 32 | 33 | Cancel 34 | 35 | 36 |
    37 | 38 | stop() ?> 39 | -------------------------------------------------------------------------------- /app/view/templates/edit.php: -------------------------------------------------------------------------------- 1 | layout('layout', ['title' => '✏ ' . $this->e($page->title()), 'stylesheets' => [ 3 | Wcms\Model::jspath() . 'edit.bundle.css', 4 | $css . 'edit.css', 5 | $css . 'tagcolors.css' 6 | ], 'favicon' => $page->favicon()]) 7 | ?> 8 | 9 | start('page') ?> 10 | 11 | 12 | 13 | insert('backtopbar', ['user' => $user, 'tab' => 'edit', 'pagelist' => $pagelist, 'pageid' => $page->id()]) ?> 14 | insert('edittopbar', ['page' => $page, 'user' => $user, 'workspace' => $workspace, 'target' => $target]) ?> 15 | 16 |
    17 | 18 | insert('editleftbar', ['page' => $page, 'pagelist' => $pagelist, 'faviconlist' => $faviconlist, 'thumbnaillist' => $thumbnaillist, 'pagelist' => $pagelist, 'editorlist' => $editorlist, 'user' => $user, 'workspace' => $workspace]) ?> 19 | insert('edittabs', ['tablist' => $page->tabs(), 'opentab' => $page->interface()]) ?> 20 | insert('editrightbar', ['page' => $page, 'workspace' => $workspace, 'homebacklink' => $homebacklink, 'urls' => $urls, 'now' => $now]) ?> 21 | 22 |
    23 | 24 | 25 | 26 | 33 | 34 | 35 | 36 | 37 | stop('page') ?> 38 | -------------------------------------------------------------------------------- /app/view/templates/edithelp.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

    update shortcut

    4 |
      5 |
    • CTRL + S
    • 6 |
    • ALT + S
    • 7 |
    8 | 9 |

    display shortcut

    10 |
      11 |
    • CTRL + D
    • 12 |
    13 | 14 |

    Search

    15 |
      16 |
    • ALT + F
    • 17 |
    • ENTER to find next.
    • 18 |
    19 | 20 |

    Replace

    21 |
      22 |
    • CTRL + SHIFT + F
    • 23 |
    24 | 25 |

    Markdown synthax

    26 |
      27 |
    • [hello](PAGE_ID/URL)link
    • 28 |
    • ![alt](imagepath)img
    • 29 |
    • <e@mail.net>
    • 30 |
    • # h1 title
    • 31 |
    • ## h2 title
    • 32 |
    • *emphasis*
    • 33 |
    • **strong**
    • 34 |
    • - list item
    • 35 |
    • > blockquote
    • 36 |
    • code
    • 37 |
    • ------horizontal line
    • 38 |
    39 | 40 |

    W synthax

    41 |
      42 |
    • [[page_id]] wiki link
    • 43 |
    • %TITLE% print page title
    • 44 |
    • %DESCRIPTION% print page description
    • 45 |
    • %THUMBNAIL% print page thumbnail
    • 46 |
    • %DATE% print date of page
    • 47 |
    • %TIME% print time of page
    • 48 |
    • %SUMMARY?option=value% generate summary
    • 49 |
    • %LIST?option=value% generate list of page
    • 50 |
    • %MEDIA?option=value% generate media list
    • 51 |
    52 | 53 |

    BODY synthax

    54 |
      55 |
    • %ELEMENT?option=value% include specified element
    • 56 |
    57 | 58 |

    59 | BODY don't support Markdown encoding. 60 |

    61 | 62 |

    More infos

    63 | 64 | 70 | -------------------------------------------------------------------------------- /app/view/templates/editrightbar.php: -------------------------------------------------------------------------------- 1 | 79 | -------------------------------------------------------------------------------- /app/view/templates/edittabs.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 | $value) : ?> 4 |
    5 | 6 | > 7 | 8 | 9 | 10 |
    11 | 12 | 22 |
    23 |
    24 | 25 | 26 |
    27 | -------------------------------------------------------------------------------- /app/view/templates/edittopbar.php: -------------------------------------------------------------------------------- 1 | 66 | -------------------------------------------------------------------------------- /app/view/templates/footer.php: -------------------------------------------------------------------------------- 1 |
    2 | Version: 3 | | Database: 4 | | Pages: 5 | | Page version: 6 |
    7 | -------------------------------------------------------------------------------- /app/view/templates/forbidden.php: -------------------------------------------------------------------------------- 1 | layout('modallayout', ['title' => 'Forbidden', 'description' => 'forbidden', 'css' => $css, 'user' => $user, 'pagelist' => $pagelist]) ?> 2 | 3 | 4 | start('modal') ?> 5 | 6 | isinvite()) : ?> 7 |

    8 | Sorry e($user->name()) ?>, you are not allowed to do this. 9 |

    10 | 11 | 12 | 13 |

    back to page read view

    14 | 15 |

    Go back 16 | 17 | 18 | stop() ?> 19 | -------------------------------------------------------------------------------- /app/view/templates/homebookmark.php: -------------------------------------------------------------------------------- 1 |

    47 | -------------------------------------------------------------------------------- /app/view/templates/info.php: -------------------------------------------------------------------------------- 1 | layout('layout', ['title' => 'Documentation', 'stylesheets' => [$css . 'back.css', $css . 'info.css']]) ?> 2 | 3 | start('page') ?> 4 | 5 | insert('backtopbar', ['user' => $user, 'tab' => 'info', 'pagelist' => $pagelist]) ?> 6 | 7 |
    8 | 9 | 25 | 26 |
    27 | 28 |

    Documentation

    29 | 30 |
    31 |
    32 | 33 |
    34 |
    35 | 36 |
    37 | 38 |
    39 | 40 | stop('page') ?> 41 | -------------------------------------------------------------------------------- /app/view/templates/layout.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <?= $title ?> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
    40 |
      41 | 42 |
    • 43 | 44 |
    • 45 | 46 |
    47 |
    48 |
    49 | 50 | 51 | section('page') ?> 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/view/templates/macro_tablesort.php: -------------------------------------------------------------------------------- 1 | sortby() === $th) : ?> 2 | 3 | -------------------------------------------------------------------------------- /app/view/templates/modallayout.php: -------------------------------------------------------------------------------- 1 | layout('layout', ['title' => $title, 'description' => $description, 'stylesheets' => [$css . 'modal.css']]) ?> 2 | 3 | start('page') ?> 4 | 5 | 6 | insert('backtopbar', ['user' => $user, 'pagelist' => $pagelist, 'tab' => null]) ?> 7 | 8 |
    9 | 15 |
    16 | 17 | stop() ?> 18 | -------------------------------------------------------------------------------- /app/view/templates/pagepassword.php: -------------------------------------------------------------------------------- 1 | layout('readerlayout') ?> 4 | 5 | start('head'); ?> 6 | 7 | 8 | 9 | 🔑 10 | 11 | ' : '' ?> 12 | 13 | 14 | stop(); ?> 15 | 16 | start('page') ?> 17 | 18 | 19 | 20 |
    21 | 22 |

    This page is password protected

    23 | 24 |
    25 | 26 | 27 | 28 |
    29 | 30 |
    31 | 32 | 33 | 34 | stop() ?> 35 | -------------------------------------------------------------------------------- /app/view/templates/profile.php: -------------------------------------------------------------------------------- 1 | layout('layout', ['title' => 'profile', 'stylesheets' => [$css . 'back.css', $css . 'profile.css']]) 4 | ?> 5 | 6 | start('page') ?> 7 | insert('backtopbar', ['user' => $user, 'tab' => 'profile', 'pagelist' => $pagelist]) ?> 8 | 9 |
    10 | 11 |
    12 | 13 |

    User profile

    14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
    idid() ?>
    connection counterconnectcount() ?>
    account expirationexpiredate('hrdi') ?>
    30 | 31 |
    32 | 33 |
    34 | 35 |

    Preferences

    36 | 37 |
    38 | 39 |

    Change some infos about you.

    40 | 41 |

    42 | 43 | 44 |

    45 | 46 |

    47 | 48 | 49 |

    50 | 51 |

    When you tick the remember-me checkbox during login, you can choose how much time W will remember you.

    52 | 53 |

    54 | 55 | 56 |

    57 | 58 |

    59 | 60 |

    61 | 62 |
    63 |
    64 | 65 | isldap()) : ?> 66 |
    67 |

    Password

    68 | 69 |
    70 | 71 |

    Password have to be between and characters long.

    72 | 73 |

    74 | 75 | 76 |

    77 | 78 |

    79 | 80 | 81 |

    82 | 83 |

    84 | 85 | 86 |

    87 | 88 |

    89 | 90 |

    91 | 92 |
    93 | 94 |
    95 | 96 | 97 |
    98 | 99 | stop('page') ?> 100 | -------------------------------------------------------------------------------- /app/view/templates/readerlayout.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | section('head')?> 6 | 7 | 8 | 9 | 10 | 11 | 12 | section('page')?> 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/view/templates/userconfirmdelete.php: -------------------------------------------------------------------------------- 1 | id() ?> 2 | layout('modallayout', ['title' => "Delete user $id", 'description' => 'delete', 'css' => $css, 'user' => $user, 'pagelist' => $pagelist]) ?> 3 | 4 | start('modal') ?> 5 | 6 | 7 | 8 | 9 |

    Id : id() ?>

    10 |

    Level : level() ?>

    11 | 12 |
    13 | 14 | 15 | 16 |
    17 | 18 | abort 19 | 20 | 21 | 22 |

    You can't delete yourself!

    23 |

    To delete this user, create at least another admin user, log in as this other admin user, then try to delete this user.

    24 |

    Go back to users

    25 | 26 | 27 | 28 | 29 | stop() ?> 30 | -------------------------------------------------------------------------------- /assets/css/admin.css: -------------------------------------------------------------------------------- 1 | /* ADMIN */ 2 | 3 | -------------------------------------------------------------------------------- /assets/css/back.css: -------------------------------------------------------------------------------- 1 | /* BACK */ 2 | /* Used everywhere except in Edit view */ 3 | 4 | /* --------------------------------------------------------- content 5 | 6 | content 7 | main layout 8 | filters 9 | footer 10 | grid layout 11 | .info icon before paragraphs 12 | icon 13 | media queries 14 | 15 | /* --------------------------------------------------------- variables */ 16 | 17 | .scroll { 18 | overflow: auto; 19 | height: 100%; 20 | max-width: 100%; 21 | } 22 | 23 | .code, code { 24 | display: block; 25 | white-space: nowrap; 26 | color: var(--code-color) !important; 27 | background-color: var(--code-background-color); 28 | padding: 2px; 29 | font-family: monospace; 30 | font-size: 15px; 31 | width: 100%; 32 | border: none; 33 | overflow: hidden; 34 | } 35 | 36 | 37 | /* Display mode links */ 38 | 39 | h2 a { 40 | color: var(--outline-background-color); 41 | } 42 | 43 | h2 a.selected, h2 a:hover { 44 | color: var(--text2-color); 45 | } 46 | 47 | 48 | /* --------------------------------------------------------- main layout */ 49 | 50 | main section { 51 | display: flex; 52 | flex-direction: column; 53 | flex: 1; 54 | background: var(--primary-background-color); 55 | gap: var(--gap, 0); 56 | flex-wrap: nowrap; 57 | width: 100%; 58 | overflow: hidden; 59 | } 60 | 61 | 62 | /* --------------------------------------------------------- filters */ 63 | /* #filter is common to Home and Media */ 64 | 65 | #filter fieldset { 66 | padding: 0; 67 | border: none; 68 | margin: 0; 69 | } 70 | 71 | #filter fieldset[data-default="0"] legend::before { 72 | content: "~ "; 73 | display: inline; 74 | position: absolute; 75 | width: 1em; 76 | left: var(--spacing); 77 | text-indent: 0; 78 | } 79 | 80 | #filter fieldset[data-default="0"] legend { 81 | text-indent: 1em; 82 | position: relative; 83 | } 84 | 85 | #filter legend .help { 86 | text-indent: 0; 87 | } 88 | 89 | #filter input[type="submit"] { 90 | width: 100%; 91 | } 92 | 93 | /* --------------------------------------------------------- footer */ 94 | 95 | footer { 96 | background-color: black; 97 | color: white; 98 | opacity: 0.4; 99 | } 100 | 101 | 102 | /* --------------------------------------------------------- grid layout */ 103 | 104 | .grid { 105 | display: grid; 106 | grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); 107 | overflow: auto; 108 | } 109 | .grid-item { 110 | background: var(--primary-background-color); 111 | padding: var(--double-spacing); 112 | display: flex; 113 | flex-direction: column; 114 | gap: var(--spacing); 115 | } 116 | .grid-item form { 117 | display: flex; 118 | flex-direction: column; 119 | gap: var(--spacing); 120 | } 121 | .grid-item a { 122 | text-decoration: underline; 123 | } 124 | .grid-item textarea { 125 | width: 100%; 126 | } 127 | .grid-item h2 { 128 | margin: calc(-1 * var(--double-spacing)) calc(-1 * var(--double-spacing)) 0 calc(-1 * var(--double-spacing)); 129 | } 130 | .grid-item h3 { 131 | margin: 1.2em 0 0; 132 | font-size: 1em; 133 | padding: var(--spacing) 0 var(--half-spacing); 134 | border-bottom: 1px solid var(--outline-background-color); 135 | 136 | } 137 | 138 | 139 | /* --------------------------------------------------------- .info icon before paragraphs */ 140 | p.info { 141 | line-height: 1.4; 142 | margin: 1.5em 0 var(--spacing); 143 | } 144 | p.info::before { 145 | content: "i"; 146 | font-family: 'Courier New', Courier, monospace; 147 | background: var(--outline-color); 148 | color: var(--outline-background-color) !important; 149 | border-radius: 100%; 150 | position: relative; 151 | top: -.25em; 152 | display: inline-flex; 153 | width: 1.2em; 154 | height: 1.2em; 155 | font-size: var(--size-small); 156 | justify-content: center; 157 | align-items: center; 158 | margin-right: 1ch; 159 | } 160 | 161 | 162 | /* --------------------------------------------------------- icon */ 163 | 164 | img.icon { 165 | height: 12px; 166 | } 167 | 168 | a:hover img.icon { 169 | filter: invert(1); 170 | } 171 | 172 | 173 | /* --------------------------------------------------------- media queries */ 174 | 175 | @media (max-width: 550px) { 176 | main { 177 | flex-direction: column; 178 | overflow-y: auto; 179 | } 180 | 181 | main > * { 182 | width: 100%; 183 | max-width: inherit !important; 184 | } 185 | 186 | } 187 | 188 | @media (pointer: coarse) { 189 | aside summary { 190 | padding-bottom: 5px; 191 | padding-top: 5px; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /assets/css/connect.css: -------------------------------------------------------------------------------- 1 | /* CONNECT */ 2 | 3 | .connect form { 4 | display: flex; 5 | flex-direction: column; 6 | gap: var(--spacing); 7 | } 8 | 9 | .connect h2 { 10 | padding: var(--spacing); 11 | border-radius: var(--radius) var(--radius) 0 0; 12 | } 13 | 14 | .connect { 15 | max-width: 250px; 16 | display: flex; 17 | flex-direction: column; 18 | gap: var(--spacing); 19 | margin: auto; 20 | border-radius: var(--radius); 21 | padding: var(--spacing); 22 | background: var(--secondary-background-color); 23 | } 24 | 25 | .button { 26 | display: block; 27 | width: 100%; 28 | text-align: center; 29 | } -------------------------------------------------------------------------------- /assets/css/home.css: -------------------------------------------------------------------------------- 1 | /* HOME */ 2 | 3 | 4 | /* --------------------------------------------------------- bookmarks */ 5 | 6 | .bookmark { 7 | display: flex; 8 | gap: var(--spacing); 9 | align-items: center; 10 | } 11 | 12 | a.bookmark { 13 | display: block; 14 | white-space: nowrap; 15 | max-width: 10em; 16 | overflow: hidden; 17 | text-overflow: ellipsis; 18 | } 19 | 20 | /* currently selected bookmark */ 21 | a.bookmark[data-current="1"] { 22 | background-color: var(--outline-background-color); 23 | color: var(--outline-color); 24 | padding: var(--half-spacing) var(--spacing); 25 | border-radius: var(--radius); 26 | } 27 | 28 | 29 | /* --------------------------------------------------------- filters */ 30 | 31 | /* for tags and authors, manage space between label and counter */ 32 | .field .label-with-counter { 33 | display: flex; 34 | } 35 | 36 | .field .label-with-counter .label { 37 | flex:1; 38 | text-overflow: ellipsis; 39 | overflow: hidden; 40 | } 41 | 42 | .field .label-with-counter .counter { 43 | border-radius: 100%; 44 | width: 1.2em; 45 | height: 1.2em; 46 | display: flex; 47 | justify-content: center; 48 | align-items: center; 49 | font-size: var(--size-small); 50 | } 51 | 52 | fieldset.tag input[type="radio"], fieldset.authors input[type="radio"] { 53 | /* make input/label closer on authors and tags radios */ 54 | margin-right: -2px; 55 | } 56 | 57 | td label, td time, 58 | td.tag, td.description { 59 | white-space: nowrap; 60 | } 61 | 62 | details#display input[type="color"] { 63 | /* limit color input height widthin Display dropdown */ 64 | height: 1.5em; 65 | } 66 | 67 | 68 | /* --------------------------------------------------------- deep search */ 69 | 70 | div#deepsearchbar { 71 | background-color: var(--secondary-background-color); 72 | padding: var(--spacing); 73 | } 74 | 75 | #deepsearchbar input[type=text] { 76 | width: 150px; 77 | } 78 | 79 | #deepsearchbar details, #deepsearchbar summary { 80 | display: inline; 81 | cursor: pointer; 82 | } 83 | 84 | /* --------------------------------------------------------- list view */ 85 | 86 | main table td.id label { 87 | font-family: monospace; 88 | font-size: var(--size-small); 89 | } 90 | 91 | #home2table a.linkto { 92 | font-family: monospace; 93 | font-size: var(--size-small); 94 | background-color: var(--main-color); 95 | color: var(--text2-color); 96 | text-wrap: nowrap; 97 | padding: 0 var(--padding); 98 | border-radius: var(--radius); 99 | } 100 | 101 | table td a.tag, table a.author { 102 | border-radius: 10px; 103 | padding: 1px 4px; 104 | } 105 | 106 | table td a.author { 107 | background-color: var(--button-background-color); 108 | color: var(--button-color); 109 | } 110 | 111 | table td a.secure{ 112 | padding: 1px 3px; 113 | color: black; 114 | } 115 | 116 | table a.secure.private { 117 | background-color: #b9b67b; 118 | } 119 | 120 | table a.secure.not_published { 121 | background-color: #b97b7b; 122 | } 123 | 124 | table td a.secure.public { 125 | background-color: #80b97b; 126 | } 127 | 128 | table .favicon img { 129 | height: 16px; 130 | max-width: 32px; 131 | } 132 | 133 | table .deadlinkcount, table .uncheckedlinkcount { 134 | border-radius: 15px; 135 | display: inline-block; 136 | height: 17px; 137 | width: 17px; 138 | text-align: center; 139 | color: white; 140 | } 141 | 142 | table .deadlinkcount { 143 | background-color: red; 144 | } 145 | 146 | table .uncheckedlinkcount { 147 | background-color: rgb(65, 65, 65); 148 | } 149 | 150 | td.title { 151 | max-width: 150px; 152 | overflow: hidden; 153 | text-overflow: ellipsis; 154 | } 155 | 156 | td.date, td.datemodif, td.datecreation { 157 | text-wrap: nowrap; 158 | } 159 | 160 | 161 | /* --------------------------------------------------------- graph view */ 162 | 163 | main div#graph { 164 | height: 100%; 165 | width: 100%; 166 | } 167 | 168 | 169 | /* --------------------------------------------------------- map view */ 170 | 171 | #map, 172 | #geomap { 173 | width: 100%; 174 | height: 100%; 175 | } 176 | #map p { 177 | padding: var(--spacing); 178 | } 179 | .leaflet-control-container { 180 | position: absolute; 181 | } 182 | 183 | 184 | /* --------------------------------------------------------- media queries */ 185 | 186 | @media (pointer: coarse) { 187 | 188 | aside #save-workspace { 189 | display: none; 190 | } 191 | a.bookmark .icon { 192 | font-size: 120%; 193 | } 194 | td.edit a, td.read a, td.delete a, td.download a { 195 | margin: 0 5px; 196 | } 197 | #deepsearchbar summary { 198 | font-size: 22px; 199 | padding: 0 10px; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /assets/css/info.css: -------------------------------------------------------------------------------- 1 | /* INFO — DOCUMENTATION */ 2 | 3 | 4 | /* --------------------------------------------------------- manual */ 5 | 6 | #manual { 7 | hyphens: auto; 8 | font-size: 1.1em; 9 | max-width: 60em; 10 | user-select: text; 11 | margin: auto; 12 | padding: var(--double-spacing); 13 | } 14 | #manual a { 15 | text-decoration: underline; 16 | text-underline-offset: .15em; 17 | color: currentColor; 18 | } 19 | 20 | #manual p { 21 | line-height: 1.4em; 22 | margin: 1em 0; 23 | } 24 | #manual li { 25 | margin: 5px 0; 26 | line-height: 23px; 27 | } 28 | 29 | #manual a[href^="https://"]::after { 30 | content: " ◥"; 31 | color: var(--main-color); 32 | } 33 | 34 | #manual a[href^="#"]::after { 35 | content: " ✻"; 36 | color: var(--main-color); 37 | white-space: nowrap; 38 | } 39 | 40 | #manual i { 41 | font-style: normal; 42 | color: #7b97b9; 43 | } 44 | 45 | #manual h1 { 46 | display: none; 47 | } 48 | 49 | #manual h2 { 50 | font-size: 40px; 51 | margin-block: 2em .5em; 52 | font-size: clamp(3em, 3vw, 6em); 53 | background: none; 54 | color: var(--text3-color); 55 | padding: 0; 56 | scroll-margin-top: 1em; 57 | } 58 | 59 | #manual h3 { 60 | background-color: var(--main-color); 61 | margin: 2em 0 1em; 62 | padding: var(--spacing); 63 | font-size: 35px; 64 | width: auto; 65 | border-width: 2px; 66 | border-radius: var(--radius); 67 | background: var(--secondary-background-color); 68 | display: inline-block; 69 | scroll-margin-top: 1em; 70 | } 71 | 72 | #manual h4 { 73 | border-bottom: solid 2px var(--main-color); 74 | font-size: x-large; 75 | margin: 1em 0; 76 | scroll-margin-top: 1em; 77 | } 78 | 79 | #manual h5 { 80 | text-transform: uppercase; 81 | margin-top: 50px; 82 | margin-bottom: 0; 83 | margin-left: 0; 84 | font-size: medium; 85 | border: solid 1px; 86 | width: fit-content; 87 | padding: 0 3px; 88 | border-left: 8px solid; 89 | border-color: var(--main-color); 90 | } 91 | 92 | #manual table { 93 | margin-top: 12px; 94 | } 95 | #manual th { 96 | background-color: unset; 97 | border-bottom: 1px solid; 98 | } 99 | 100 | #manual th, 101 | #manual td { 102 | padding: var(--spacing) 0; 103 | } 104 | 105 | #manual ul { 106 | list-style: inside disc; 107 | padding-inline-start: 10px; 108 | } 109 | 110 | #manual pre { 111 | padding: var(--half-spacing) var(--spacing); 112 | background-color: var(--code-color); 113 | white-space: pre-wrap; 114 | border-radius: var(--radius); 115 | line-height: 1.4; 116 | } 117 | 118 | #manual kbd { 119 | background-color: var(--main-color); 120 | color: var(--text2-color); 121 | padding: 1px 4px; 122 | border-radius: 7px; 123 | box-shadow: 2px 2px; 124 | margin: 0 2px; 125 | } 126 | #manual code { 127 | width: fit-content; 128 | font-family: monospace; 129 | white-space: pre-wrap; 130 | display: inline; 131 | padding: var(--half-spacing); 132 | border-radius: var(--radius); 133 | background-color: var(--code-color); 134 | color: var(--code-background-color) !important; 135 | } 136 | 137 | #manual pre code { 138 | padding: 0; 139 | } 140 | 141 | #manual blockquote { 142 | margin: 30px 0; 143 | margin-left: 15px; 144 | padding-left: 5px; 145 | border-left: solid 3px var(--main-color); 146 | } 147 | 148 | 149 | /* --------------------------------------------------------- media queries */ 150 | 151 | @media (max-width: 750px) { 152 | #manual { 153 | font-size: 1em; 154 | } 155 | } 156 | 157 | 158 | @media (max-width: 550px) { 159 | main { 160 | display: block; 161 | overflow: scroll; 162 | } 163 | } 164 | 165 | 166 | /* --------------------------------------------------------- toc */ 167 | 168 | #toc { 169 | overflow: hidden; /* allow scroll */ 170 | min-width: 180px; 171 | background: var(--primary-background-color); 172 | padding-bottom: 2em; 173 | } 174 | 175 | #toc .scroll > :not(h2){ 176 | padding: var(--double-spacing); 177 | } 178 | 179 | .summary { 180 | padding: var(--spacing); 181 | line-height: 1.4; 182 | } 183 | 184 | .summary li a { 185 | text-decoration: none; 186 | } 187 | 188 | .summary li a:hover { 189 | text-decoration: underline; 190 | } 191 | 192 | .summary > li { 193 | margin-bottom: 10px; 194 | } 195 | 196 | .summary > li > a { 197 | font-weight: bold; 198 | font-size: 1.15em; 199 | } 200 | 201 | .summary > li > ul > li > ul { 202 | font-size: var(--size-small); 203 | border-left: solid 2px var(--main-color); 204 | padding-left: 5px; 205 | margin-left: 5px; 206 | margin-block: 6px; 207 | opacity: 0.9; 208 | } 209 | 210 | .summary > li > ul { 211 | padding-left: 15px; 212 | } 213 | -------------------------------------------------------------------------------- /assets/css/modal.css: -------------------------------------------------------------------------------- 1 | /* MODAL */ 2 | 3 | .modal form { 4 | display: flex; 5 | flex-direction: column; 6 | gap: var(--spacing); 7 | } 8 | 9 | .modal h2 { 10 | padding: var(--spacing); 11 | border-radius: var(--radius) var(--radius) 0 0; 12 | } 13 | 14 | .modal { 15 | max-width: 500px; 16 | display: flex; 17 | flex-direction: column; 18 | gap: var(--spacing); 19 | margin: auto; 20 | border-radius: var(--radius); 21 | padding: var(--spacing); 22 | background: var(--secondary-background-color); 23 | } 24 | 25 | .button { 26 | display: block; 27 | width: 100%; 28 | text-align: center; 29 | } 30 | -------------------------------------------------------------------------------- /assets/css/profile.css: -------------------------------------------------------------------------------- 1 | /* PROFILE */ 2 | -------------------------------------------------------------------------------- /assets/css/theme/audrey-s-book.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text-color: #272f09; 3 | --text2-color: #595d1e; 4 | --text3-color: #3b1338; 5 | --main-color: #eaeeaf; 6 | --secondary-background-color: #d2bd76; 7 | --code-background-color: hsl(213 21% 10% / 1); 8 | --code-color: hsl(89.26deg 46% 66%); 9 | --primary-background-color: #ffefbc; 10 | --tertiary-background-color: #c6b98e; 11 | --outline-background-color: #e7c969; 12 | --outline-color: #262626; 13 | --button-background-color: #dcdd98; 14 | --button-color: #000000; 15 | --input-background-color: #ffffff; 16 | --input-color: #303030; 17 | color-scheme: light; 18 | 19 | --radius: 4px; 20 | } 21 | 22 | .cm-wcms, .cm-wkeyword, .editor textarea, .CodeMirror { 23 | color: var(--text-color); 24 | background-color: var(--main-color); 25 | border-radius: 0; 26 | } 27 | 28 | 29 | .CodeMirror, .editor #editmain, .editor #editheader, .editor #editnav, .editor #editaside, .editor #editfooter { 30 | font-family: serif; 31 | } 32 | -------------------------------------------------------------------------------- /assets/css/theme/blue-whale.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text-color: #dfe0eb; 3 | --text2-color: #ffffff; 4 | --text3-color: #3b1338; 5 | --main-color: #033879; 6 | --secondary-background-color: #4050bd; 7 | --code-background-color: hsl(213deg 74.31% 13.58%); 8 | --code-color: hsl(213deg 69.63% 74.23%); 9 | --primary-background-color: #5476d9; 10 | --tertiary-background-color: #061456; 11 | --outline-background-color: #1927cf; 12 | --outline-color: #ffffff; 13 | --button-background-color: #0057d9; 14 | --button-color: #ffffff; 15 | --input-background-color: #002152; 16 | --input-color: #ffffff; 17 | color-scheme: dark; 18 | 19 | --radius: 1em; 20 | } 21 | 22 | button, input[type="submit"], input[type="text"] { 23 | border: solid 1px var(--text-color); 24 | } 25 | 26 | 27 | .submenu, .block, nav.bar { 28 | border-radius: 0 0 10px 10px; 29 | } 30 | -------------------------------------------------------------------------------- /assets/css/theme/dark-doriphore.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text-color: #ffffff; 3 | --text2-color: #ffeb01; 4 | --text3-color: #ffe001; 5 | --main-color: #4c0780; 6 | --secondary-background-color: #000000; 7 | --code-background-color: rgb(255 128 19); 8 | --code-color: #000000; 9 | --primary-background-color: #414141; 10 | --tertiary-background-color: #000000; 11 | --outline-background-color: #ffb101; 12 | --outline-color: #000000; 13 | --button-background-color: #ff662c; 14 | --button-color: #000000; 15 | --input-background-color: #000000; 16 | --input-color: #ff662c; 17 | color-scheme: dark; 18 | --font-family: monospace; 19 | --radius: 0; 20 | } 21 | 22 | input, button, select, textarea, .dropdown, .dropdown-content { 23 | border-radius: 0px; 24 | border: solid 1px; 25 | } 26 | .dropdown-content { 27 | border: solid 1px; 28 | margin-left: -1px; 29 | padding-top: 3px; 30 | } 31 | 32 | body { 33 | font-family: monospace; 34 | font-size: 13px; 35 | } 36 | 37 | aside { 38 | border-bottom: solid 1px; 39 | } 40 | -------------------------------------------------------------------------------- /assets/css/theme/default.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text-color: black; 3 | --text2-color: #ffffff; 4 | --text3-color: #3b1338; 5 | --main-color: #7b97b9; 6 | --secondary-background-color: #bbbbbb; 7 | --code-background-color: hsl(213 21% 10% / 1); 8 | --code-color: hsl(213deg 100% 80.86%); 9 | --primary-background-color: #d6d6d6; 10 | --tertiary-background-color: #909090; 11 | --outline-background-color: #7a7a7a; 12 | --outline-color: #ffffff; 13 | --button-background-color: #ecf0f5; 14 | --button-color: #000000; 15 | --input-background-color: #ffffff; 16 | --input-color: #303030; 17 | color-scheme: light; 18 | } 19 | 20 | :root { 21 | --radius: 3px; 22 | --spacing: 6px; 23 | --padding: 3px; 24 | } -------------------------------------------------------------------------------- /assets/css/theme/funky-freddy.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text-color: #5e0063; 3 | --text2-color: #9f075d; 4 | --text3-color: #70360e; 5 | --main-color: #10f6ba; 6 | --secondary-background-color: #ff9090; 7 | --code-background-color: rgb(75 15 83); 8 | --code-color: #f8ff00; 9 | --primary-background-color: #cdcdcd; 10 | --tertiary-background-color: #8e98bc; 11 | --outline-background-color: #119b77; 12 | --outline-color: #fff; 13 | --button-background-color: #e9e9e9; 14 | --button-color: #000000; 15 | --input-background-color: #ffffff; 16 | --input-color: #303030; 17 | color-scheme: light; 18 | 19 | --radius: 5px 20 | } 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/css/theme/fuzzy-flamingo.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text-color: black; 3 | --text2-color: #ffffff; 4 | --text3-color: #3b1338; 5 | --main-color: #fb21d6; 6 | --secondary-background-color: #e589d5; 7 | --code-background-color: rgb(17 10 54); 8 | --code-color: #e381fe; 9 | --primary-background-color: #ecbbe3; 10 | --tertiary-background-color: #fd88ff; 11 | --outline-background-color: #2e1530; 12 | --outline-color: #ffffff; 13 | --button-background-color: #b707c9; 14 | --button-color: #ffffff; 15 | --input-background-color: #eab7f0; 16 | --input-color: #9621a4; 17 | color-scheme: light; 18 | } 19 | 20 | input, button, select, textarea { 21 | border-radius: 5px; 22 | border: solid 1px var(--secondary-background-color); 23 | } 24 | 25 | 26 | 27 | input[type="submit"], button { 28 | border: none; 29 | } 30 | 31 | 32 | 33 | .block { 34 | box-shadow: -4px 8px 20px #ff00c69c; 35 | border-radius: 5px; 36 | } 37 | 38 | main > * { 39 | padding: 5px; 40 | } 41 | -------------------------------------------------------------------------------- /assets/css/theme/industrial-dream.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text-color: black; 3 | --text2-color: #ffffff; 4 | --text3-color: #3b1338; 5 | --main-color: #848484; 6 | --secondary-background-color: #bbbbbb; 7 | --code-background-color: hsl(213 21% 10% / 1); 8 | --code-color: hsl(0deg 0% 100%); 9 | --primary-background-color: #d6d6d6; 10 | --tertiary-background-color: #909090; 11 | --outline-background-color: #5e5e5e; 12 | --outline-color: #ffffff; 13 | --button-background-color: #ecf0f5; 14 | --button-color: #000000; 15 | --input-background-color: #ffffff; 16 | --input-color: #303030; 17 | color-scheme: light; 18 | 19 | --radius: 0; 20 | } 21 | 22 | input, textarea { 23 | border: inset 2px; 24 | border-color: #333 #666 #666 #333; 25 | } 26 | 27 | input[type="submit"], button, select { 28 | border: outset 2px; 29 | } 30 | 31 | .block { 32 | border: ridge 2px; 33 | margin: 0px; 34 | } 35 | 36 | 37 | 38 | 39 | .submenu { 40 | border: ridge 2px; 41 | } 42 | 43 | h2, header { 44 | border-bottom: #757575 solid 1px; 45 | } 46 | 47 | div.panel details { 48 | border: solid 1px; 49 | border-color: var(--tertiary-background-color) 50 | } 51 | -------------------------------------------------------------------------------- /assets/css/theme/soy-n-wasabi.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text-color: black; 3 | --text2-color: #e8e8e8; 4 | --text3-color: #13173b; 5 | --main-color: rgb(38, 39, 43); 6 | --secondary-background-color: #a5c12b; 7 | --code-background-color: hsl(213 21% 10% / 1); 8 | --code-color: hsl(0deg 0% 100%); 9 | --primary-background-color: #d6d6d6; 10 | --tertiary-background-color: #909090; 11 | --outline-background-color: #527028; 12 | --outline-color: #ffffff; 13 | --button-background-color: #ecf0f5; 14 | --button-color: #0f0f1b; 15 | --input-background-color: #ffffff; 16 | --input-color: #303030; 17 | color-scheme: light; 18 | 19 | --spacing: .5rem; 20 | --radius: .25rem; 21 | --font-family: "CommitMono", "Fira Mono", monospace; 22 | --gap: 1px; 23 | } 24 | -------------------------------------------------------------------------------- /assets/css/user.css: -------------------------------------------------------------------------------- 1 | /* USER */ 2 | 3 | 4 | main.user { 5 | display: block; 6 | overflow: scroll; 7 | } 8 | .flexrow { 9 | padding: var(--spacing); 10 | align-items: end; 11 | } 12 | 13 | table { 14 | background-color: var(--primary-background-color); 15 | } 16 | 17 | .new-user { 18 | /* minimum height for new user section */ 19 | flex: 0; 20 | } 21 | 22 | .submit-field { 23 | /* minimum width for new user submit button */ 24 | flex: 0; 25 | } 26 | 27 | /* --------------------------------------------------------- media queries */ 28 | 29 | @media (max-width: 750px) { 30 | #manual { 31 | font-size: 1em; 32 | } 33 | } 34 | 35 | @media (max-width:700px) { 36 | /* inverse direction for new user form */ 37 | .flexrow { 38 | flex-direction: column; 39 | align-items: start; 40 | justify-content: stretch; 41 | } 42 | } 43 | 44 | @media (max-width: 550px) { 45 | /* quick n dirty barely ussable table */ 46 | th { 47 | display: none; 48 | } 49 | tr { 50 | display: flex; 51 | flex-direction: column; 52 | border-bottom: 3px solid; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /assets/fonts/forkawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincent-peugnet/wcms/4a9d723bbcc35b64a68e5d410315f11b321fafb0/assets/fonts/forkawesome-webfont.eot -------------------------------------------------------------------------------- /assets/fonts/forkawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincent-peugnet/wcms/4a9d723bbcc35b64a68e5d410315f11b321fafb0/assets/fonts/forkawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/fonts/forkawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincent-peugnet/wcms/4a9d723bbcc35b64a68e5d410315f11b321fafb0/assets/fonts/forkawesome-webfont.woff -------------------------------------------------------------------------------- /assets/fonts/forkawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincent-peugnet/wcms/4a9d723bbcc35b64a68e5d410315f11b321fafb0/assets/fonts/forkawesome-webfont.woff2 -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "reach, diff" 3 | branches: # branch names that can post comment 4 | - "master" -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "w-cms/w-cms", 3 | "description": "point'n think", 4 | "license": "AGPL-3.0-or-later", 5 | "require": { 6 | "php": ">=7.4.0", 7 | "altorouter/altorouter": "^1.2", 8 | "firebase/php-jwt": "^5.2", 9 | "jamesmoss/flywheel": "^0.5.2", 10 | "league/plates": "^3.3", 11 | "michelf/php-markdown": "^1.8", 12 | "vstelmakh/url-highlight": "^3.0" 13 | }, 14 | "require-dev": { 15 | "filp/whoops": "^2.7", 16 | "pepakriz/phpstan-exception-rules": "^0.12", 17 | "phpstan/phpstan": "^1.0", 18 | "phpstan/phpstan-phpunit": "^1.0", 19 | "phpunit/phpunit": "^9.0", 20 | "sentry/sdk": "^3.1", 21 | "squizlabs/php_codesniffer": "^3.5" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Wcms\\": "app/class" 26 | }, 27 | "files": ["app/fn/fn.php"] 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Wcms\\Tests\\": "tests" 32 | }, 33 | "files": ["tests/fn.php"] 34 | }, 35 | "config": { 36 | "platform": { 37 | "php": "7.4" 38 | }, 39 | "sort-packages": true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | getMessage()); 10 | } 11 | 12 | $app = new Wcms\Application(); 13 | $app->wakeup(); 14 | 15 | session_set_cookie_params([ 16 | 'path' => '/' . Wcms\Config::basepath(), 17 | 'samesite' => 'Strict', 18 | 'secure' => Wcms\Config::issecure() 19 | ]); 20 | session_start(); 21 | 22 | if (class_exists('Whoops\Run') && !empty(Wcms\Config::debug())) { 23 | $whoops = new \Whoops\Run(); 24 | $handler = new \Whoops\Handler\PrettyPageHandler(); 25 | $handler->setEditor(\Wcms\Config::debug()); 26 | $whoops->pushHandler($handler); 27 | $whoops->register(); 28 | } 29 | 30 | if (isreportingerrors()) { 31 | Sentry\init([ 32 | 'dsn' => Wcms\Config::sentrydsn(), 33 | 'release' => getversion(), 34 | ]); 35 | Sentry\configureScope(function ($scope) { 36 | $scope->setUser([ 37 | 'id' => Wcms\Config::url(), 38 | 'username' => Wcms\Config::basepath(), 39 | ]); 40 | }); 41 | } 42 | 43 | try { 44 | $matchoper = new Wcms\Routes(); 45 | $matchoper->match(); 46 | } catch (Throwable $e) { 47 | if (isreportingerrors()) { 48 | Sentry\captureException($e); 49 | } 50 | Wcms\Logger::errorex($e, true); 51 | http_response_code(500); 52 | if (isset($whoops)) { 53 | $whoops->handleException($e); 54 | } 55 | echo '

    ⚠ Whoops ! There is a little problem :

    '; 56 | echo `

    ` . $e->getMessage() . '

    '; 57 | echo '

    Please contact yout Wiki admin to solve this.

    '; 58 | } 59 | Wcms\Logger::close(); 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wcms", 3 | "repository": "github:vincent-peugnet/wcms", 4 | "license": "MIT", 5 | "dependencies": { 6 | "@yaireo/tagify": "^4.21.1", 7 | "codemirror": "^5.63.3", 8 | "cytoscape": "^3.20.0", 9 | "cytoscape-cose-bilkent": "^4.1.0", 10 | "cytoscape-euler": "^1.2.3", 11 | "cytoscape-fcose": "^2.2.0", 12 | "leaflet": "^1.9.3" 13 | }, 14 | "devDependencies": { 15 | "@sentry/browser": "^7.119.1", 16 | "@sentry/cli": "^2.8.1", 17 | "esbuild": "^0.20.0", 18 | "prettier": "^1.19.1", 19 | "semver": "^7.3.8" 20 | }, 21 | "prettier": { 22 | "tabWidth": 4, 23 | "trailingComma": "es5", 24 | "singleQuote": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | The coding standard for Wcms. 6 | 7 | 8 | index.php 9 | app 10 | tests 11 | app/view/templates/*\.php$ 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpstan/phpstan-phpunit/extension.neon 3 | - vendor/pepakriz/phpstan-exception-rules/extension.neon 4 | 5 | parameters: 6 | level: 5 7 | paths: 8 | - app 9 | - index.php 10 | - tests 11 | excludePaths: 12 | - app/view/* 13 | dynamicConstantNames: 14 | - INTL_ICU_VERSION 15 | exceptionRules: 16 | # ignore some exceptions and their chlidrens 17 | uncheckedExceptions: 18 | - Error 19 | - LogicException 20 | # ignore all exceptions errors in tests classes 21 | methodWhitelist: 22 | PHPUnit\Framework\TestCase: '#.*#i' 23 | tmpDir: build/phpstan 24 | treatPhpDocTypesAsCertain: false 25 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | app/class 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/fn/fn.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Manage input event on the checkall checkbox. 3 | * Call with .bind({checkboxes: HTMLElement[]}) 4 | * @param {InputEvent} e the input event 5 | */ 6 | function checkallHandler(e) { 7 | if (e.target.checked) { 8 | for (const checkbox of this.checkboxes) { 9 | checkbox.checked = true; 10 | } 11 | } else { 12 | for (const checkbox of this.checkboxes) { 13 | checkbox.checked = false; 14 | } 15 | } 16 | } 17 | 18 | /** 19 | * Activate the checkall feature 20 | * @param {string} checkboxesName value of the name property of the desired checkbox elements. 21 | * @param {string} checkallId value of the id property of the desired checkall element. 22 | */ 23 | export function activateCheckall(checkboxesName, checkallId) { 24 | let checkboxes = document.getElementsByName(checkboxesName); 25 | let checkall = document.getElementById(checkallId); 26 | if (!checkall) { 27 | return; 28 | } 29 | let checkbox = document.createElement('input'); 30 | checkbox.type = 'checkbox'; 31 | checkbox.addEventListener('input', checkallHandler.bind({ checkboxes })); 32 | checkall.innerHTML = ''; 33 | checkall.appendChild(checkbox); 34 | } 35 | 36 | /** 37 | * Close all submenus of the menubar. 38 | * @param {MouseEvent} e 39 | */ 40 | function closeSubmenus(e) { 41 | let details = document.querySelectorAll('.dropdown'); 42 | let currentDetail = e.target.closest('.dropdown'); 43 | for (const detail of details) { 44 | if (!detail.isSameNode(currentDetail)) { 45 | detail.removeAttribute('open'); 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * Activate "close submenus" feature on click anywhere. 52 | */ 53 | export function activateCloseSubmenus() { 54 | window.addEventListener('click', closeSubmenus); 55 | } 56 | 57 | /** 58 | * Manage submit event for a given form 59 | * @param {HTMLFormElement} form 60 | * @param {(response: any) => void} onSuccess 61 | */ 62 | export function submitHandler(form, onSuccess = () => {}) { 63 | var xhr = new XMLHttpRequest(); 64 | var fd = new FormData(form); 65 | 66 | xhr.addEventListener('load', function(event) { 67 | if (httpOk(xhr.status)) { 68 | onSuccess(xhr.response); 69 | } else { 70 | alert( 71 | '⚠️ Error while trying to update:\ncopy your work and refresh the current page\n\nAPI response code: ' + 72 | xhr.status + 73 | ' ' + 74 | xhr.statusText 75 | ); 76 | } 77 | }); 78 | xhr.addEventListener('error', function(event) { 79 | alert('Network error while trying to update.'); 80 | }); 81 | xhr.open(form.method, form.dataset.api); 82 | xhr.send(fd); 83 | } 84 | 85 | /** 86 | * Check if an HTTP response status indicates a success. 87 | * @param {number} status 88 | */ 89 | function httpOk(status) { 90 | return status >= 200 && status < 300; 91 | } 92 | 93 | export function initWorkspaceForm() { 94 | let form = document.getElementById('workspace-form'); 95 | let inputs = form.elements; 96 | for (const input of inputs) { 97 | input.oninput = workspaceChanged; 98 | } 99 | let saveworkspace = document.getElementById('save-workspace'); 100 | if (saveworkspace instanceof HTMLElement) { 101 | saveworkspace.style.display = 'none'; 102 | } 103 | 104 | form.addEventListener('submit', function(event) { 105 | event.preventDefault(); 106 | submitHandler(this); 107 | }); 108 | } 109 | 110 | /** 111 | * @param {InputEvent} e 112 | */ 113 | function workspaceChanged(e) { 114 | let elem = e.target; 115 | elem.form.requestSubmit(); 116 | } 117 | -------------------------------------------------------------------------------- /src/graph.js: -------------------------------------------------------------------------------- 1 | import cytoscape from 'cytoscape'; 2 | import coseBilkent from 'cytoscape-cose-bilkent'; 3 | import fcose from 'cytoscape-fcose'; 4 | import euler from 'cytoscape-euler'; 5 | 6 | cytoscape.use(euler); 7 | cytoscape.use(fcose); 8 | cytoscape.use(coseBilkent); 9 | 10 | let options = { 11 | container: document.getElementById('graph'), 12 | }; 13 | 14 | Object.assign(options, data); 15 | 16 | let cy = cytoscape(options); 17 | 18 | cy.on('tap', 'node', function() { 19 | try { 20 | // your browser may block popups 21 | window.open(this.data('leftclick')); 22 | } catch (e) { 23 | // fall back on url change 24 | window.location.href = this.data('leftclick'); 25 | } 26 | }); 27 | 28 | cy.on('cxttap', 'node', function() { 29 | try { 30 | // your browser may block popups 31 | window.open(this.data('edit')); 32 | } catch (e) { 33 | // fall back on url change 34 | window.location.href = this.data('edit'); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /src/home.js: -------------------------------------------------------------------------------- 1 | import { 2 | activateCheckall, 3 | activateCloseSubmenus, 4 | initWorkspaceForm, 5 | } from './fn/fn'; 6 | 7 | window.addEventListener('load', () => { 8 | activateCheckall('pagesid[]', 'checkall'); 9 | activateCloseSubmenus(); 10 | initWorkspaceForm(); 11 | }); 12 | -------------------------------------------------------------------------------- /src/map.js: -------------------------------------------------------------------------------- 1 | import 'leaflet/dist/leaflet.css'; 2 | import icon from 'leaflet/dist/images/marker-icon.png'; 3 | import icon_2x from 'leaflet/dist/images/marker-icon-2x.png'; 4 | import shadow from 'leaflet/dist/images/marker-shadow.png'; 5 | import * as L from 'leaflet'; 6 | 7 | L.Icon.Default.prototype.options.iconUrl = icon; 8 | L.Icon.Default.prototype.options.iconRetinaUrl = icon_2x; 9 | L.Icon.Default.prototype.options.shadowUrl = shadow; 10 | 11 | var map = L.map('geomap').setView([43.3, 6.68], 1); 12 | 13 | var pageGroup = L.featureGroup(); 14 | pageGroup.addTo(map); 15 | 16 | L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { 17 | attribution: 18 | '© OpenStreetMap contributors', 19 | }).addTo(map); 20 | 21 | for (const page of pages) { 22 | L.marker([page.latitude, page.longitude]) 23 | .addTo(pageGroup) 24 | .bindPopup( 25 | `${page.title} ` 26 | ); 27 | } 28 | 29 | map.fitBounds(pageGroup.getBounds()); 30 | -------------------------------------------------------------------------------- /src/media.js: -------------------------------------------------------------------------------- 1 | import { 2 | activateCheckall, 3 | activateCloseSubmenus, 4 | initWorkspaceForm, 5 | } from './fn/fn'; 6 | 7 | window.addEventListener('load', () => { 8 | activateCheckall('id[]', 'checkall'); 9 | activateCloseSubmenus(); 10 | initWorkspaceForm(); 11 | }); 12 | 13 | /** 14 | * Drag and drop files anywhere to fill regular file input 15 | */ 16 | const fileInput = document.getElementById('file'); 17 | const fileMenu = fileInput.closest('details'); 18 | const droppedFilesSet = new Set(); // Track dropped files 19 | 20 | const allDragAndDropListeners = [ 21 | 'drag', 22 | 'dragstart', 23 | 'dragend', 24 | 'dragover', 25 | 'dragenter', 26 | 'dragleave', 27 | 'drop', 28 | ]; 29 | for (const listener of allDragAndDropListeners) { 30 | document.body.addEventListener( 31 | listener, 32 | function(e) { 33 | e.preventDefault(); 34 | e.stopPropagation(); 35 | }, 36 | false 37 | ); 38 | } 39 | 40 | const dragListeners = ['dragover', 'dragenter']; 41 | for (const listener of dragListeners) { 42 | document.body.addEventListener( 43 | listener, 44 | function(e) { 45 | fileMenu.setAttribute('open', true); 46 | }, 47 | false 48 | ); 49 | } 50 | 51 | document.body.addEventListener( 52 | 'drop', 53 | function(e) { 54 | const droppedFiles = e.dataTransfer.files; 55 | const fileList = new DataTransfer(); 56 | 57 | // Prepare user feeedback 58 | const info = 59 | document.querySelector('.dropped-files-info') || 60 | document.createElement('p'); 61 | 62 | // If feedback element doesn’t exist 63 | if (!document.querySelector('.dropped-files-info')) { 64 | info.className = 'dropped-files-info'; 65 | fileInput.insertAdjacentElement('afterend', info); 66 | } 67 | 68 | // Loop through the currently existing files in the file input 69 | for (let i = 0; i < fileInput.files.length; i++) { 70 | fileList.items.add(fileInput.files[i]); 71 | // Track already added files 72 | droppedFilesSet.add( 73 | fileInput.files[i].name + fileInput.files[i].size 74 | ); 75 | } 76 | 77 | // append only new files to droppedFiles array 78 | for (const file of droppedFiles) { 79 | const uniqueID = file.name + file.size; 80 | 81 | // Check if the file is already added 82 | if (!droppedFilesSet.has(uniqueID)) { 83 | fileList.items.add(file); // Add file if it's not already added 84 | droppedFilesSet.add(uniqueID); // Add to the tracking set 85 | info.innerHTML += `${file.name} `; // Feedback 86 | } 87 | } 88 | 89 | fileInput.files = fileList.files; 90 | }, 91 | false 92 | ); 93 | -------------------------------------------------------------------------------- /src/pagemap.js: -------------------------------------------------------------------------------- 1 | import 'leaflet/dist/leaflet.css'; 2 | import icon from 'leaflet/dist/images/marker-icon.png'; 3 | import icon_2x from 'leaflet/dist/images/marker-icon-2x.png'; 4 | import shadow from 'leaflet/dist/images/marker-shadow.png'; 5 | import * as L from 'leaflet'; 6 | 7 | L.Icon.Default.prototype.options.iconUrl = icon; 8 | L.Icon.Default.prototype.options.iconRetinaUrl = icon_2x; 9 | L.Icon.Default.prototype.options.shadowUrl = shadow; 10 | 11 | var map = L.map(mapId).setView([0, 0], 1); 12 | 13 | var pageGroup = L.featureGroup(); 14 | pageGroup.addTo(map); 15 | 16 | L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { 17 | attribution: 18 | '© OpenStreetMap contributors', 19 | }).addTo(map); 20 | 21 | for (const page of pages) { 22 | L.marker([page.latitude, page.longitude]) 23 | .addTo(pageGroup) 24 | .bindPopup(`${page.title}`); 25 | } 26 | 27 | map.fitBounds(pageGroup.getBounds()); 28 | -------------------------------------------------------------------------------- /src/sentry.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/browser'; 2 | 3 | // All these values come from PHP templates 4 | Sentry.init({ 5 | dsn: sentrydsn, 6 | release: version, 7 | }); 8 | Sentry.setUser({ 9 | id: url, 10 | username: basepath, 11 | }); 12 | -------------------------------------------------------------------------------- /tests/.gitattributes: -------------------------------------------------------------------------------- 1 | *.html text eol=lf 2 | -------------------------------------------------------------------------------- /tests/FilesTest.php: -------------------------------------------------------------------------------- 1 | testdir 10 | * - $this->notwritabledir 11 | * - $this->notwritablefile 12 | */ 13 | abstract class FilesTest extends TestCase 14 | { 15 | protected $ds = DIRECTORY_SEPARATOR; 16 | protected $testdir = 'build/test'; 17 | protected $notwritabledir = 'build/test/notwritabledir'; 18 | protected $notwritablefile = 'build/test/notwritablefile'; 19 | 20 | protected function setUp(): void 21 | { 22 | parent::setUp(); 23 | if (!is_dir($this->testdir)) { 24 | mkdir($this->testdir, 0755, true); 25 | } 26 | if (!file_exists($this->notwritabledir)) { 27 | mkdir($this->notwritabledir, 0000); 28 | } 29 | if (!file_exists($this->notwritablefile)) { 30 | touch($this->notwritablefile); 31 | chmod($this->notwritablefile, 0000); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Servicerenderv1Test.php: -------------------------------------------------------------------------------- 1 | cwd = getcwd(); 30 | chdir("tests/fixtures"); 31 | 32 | $router = new AltoRouter([ 33 | ['GET', '/[cid:page]', 'Controllerpage#read', 'pageread'], 34 | ]); 35 | $this->renderengine = new Servicerenderv1($router, new Modelpage(Config::pagetable()), true, false); 36 | } 37 | 38 | public function tearDown(): void 39 | { 40 | chdir($this->cwd); 41 | parent::tearDown(); 42 | } 43 | 44 | public function renderTest(string $name): void 45 | { 46 | $pagedata = json_decode(file_get_contents(__DIR__ . "/data/Servicerenderv1Test/$name.json"), true); 47 | $page = new Pagev1($pagedata); 48 | $html = $this->renderengine->render($page); 49 | 50 | $expected = __DIR__ . "/data/Servicerenderv1Test/$name.html"; 51 | $actual = self::$tmpdir . "/$name.html"; 52 | 53 | $doc = new DOMDocument(); 54 | $doc->loadHTML($html, LIBXML_NOERROR); 55 | $body = $doc->getElementsByTagName("body")->item(0); 56 | $body = $doc->saveHTML($body) . "\n"; 57 | 58 | Fs::writefile($actual, $body); 59 | 60 | $this->assertFileEquals($expected, $actual, "$actual render does not match expected $expected"); 61 | } 62 | 63 | /** 64 | * @test 65 | * @dataProvider renderProvider 66 | */ 67 | public function renderTestCommon(string $name): void 68 | { 69 | $this->renderTest($name); 70 | } 71 | 72 | /** 73 | * @return array[] 74 | */ 75 | public function renderProvider(): array 76 | { 77 | return [ 78 | ['empty-test'], 79 | ['markdown-test'], 80 | ['markdown-test-2'], 81 | ['body-test'], 82 | ['external-links-test'], 83 | ]; 84 | } 85 | 86 | /** 87 | * @test 88 | * @requires OS Linux 89 | * @requires extension intl 90 | */ 91 | public function renderTestDate(): void 92 | { 93 | if (floatval(INTL_ICU_VERSION) >= 72) { 94 | $this->markTestSkipped(); 95 | } 96 | $this->renderTest('date-time-test'); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Servicerenderv2Test.php: -------------------------------------------------------------------------------- 1 | cwd = getcwd(); 30 | chdir("tests/fixtures"); 31 | 32 | $router = new AltoRouter([ 33 | ['GET', '/[cid:page]', 'Controllerpage#read', 'pageread'], 34 | ]); 35 | $this->renderengine = new Servicerenderv2($router, new Modelpage(Config::pagetable()), true, false); 36 | } 37 | 38 | public function tearDown(): void 39 | { 40 | chdir($this->cwd); 41 | parent::tearDown(); 42 | } 43 | 44 | public function renderTest(string $name): void 45 | { 46 | $pagedata = json_decode(file_get_contents(__DIR__ . "/data/Servicerenderv2Test/$name.json"), true); 47 | $page = new Pagev2($pagedata); 48 | $html = $this->renderengine->render($page); 49 | 50 | $expected = __DIR__ . "/data/Servicerenderv2Test/$name.html"; 51 | $actual = self::$tmpdir . "/$name.html"; 52 | 53 | $doc = new DOMDocument(); 54 | $doc->loadHTML($html, LIBXML_NOERROR); 55 | $body = $doc->getElementsByTagName("body")->item(0); 56 | $body = $doc->saveHTML($body) . "\n"; 57 | 58 | Fs::writefile($actual, $body); 59 | 60 | $this->assertFileEquals($expected, $actual, "$actual render does not match expected $expected"); 61 | } 62 | 63 | /** 64 | * @test 65 | * @dataProvider renderProvider 66 | */ 67 | public function renderTestCommon(string $name): void 68 | { 69 | $this->renderTest($name); 70 | } 71 | 72 | /** 73 | * @return array[] 74 | */ 75 | public function renderProvider(): array 76 | { 77 | return [ 78 | ['empty-test-v2'], 79 | ['markdown-test-v2'], 80 | ['body-test-v2'], 81 | ['external-links-test-v2'], 82 | ]; 83 | } 84 | 85 | /** 86 | * @test 87 | * @requires OS Linux 88 | * @requires extension intl 89 | */ 90 | public function renderTestDate(): void 91 | { 92 | if (floatval(INTL_ICU_VERSION) >= 72) { 93 | $this->markTestSkipped(); 94 | } 95 | $this->renderTest('date-time-test-v2'); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/SummaryTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $summary->sumparser()); 19 | } 20 | 21 | /** 22 | * @return array[] 23 | */ 24 | public function sumparserProvider(): array 25 | { 26 | return [ 27 | [ 28 | [ 29 | 'sum' => [ 30 | new Header('test-1', 1, 'Test 1'), 31 | new Header('test-1-2', 2, 'Test 1.2') 32 | ] 33 | ], 34 | // phpcs:ignore Generic.Files.LineLength.TooLong 35 | '' 36 | ], 37 | [ 38 | [ 39 | 'sum' => [ 40 | new Header('test-1', 1, 'Test 1'), 41 | new Header('test-1-2', 2, 'Test 1.2') 42 | ], 43 | 'options' => 'min=2' 44 | ], 45 | '' 46 | ], 47 | [ 48 | [ 49 | 'sum' => [ 50 | new Header('test-1', 1, 'Test 1'), 51 | new Header('test-1-2', 2, 'Test 1.2') 52 | ], 53 | 'options' => 'max=1' 54 | ], 55 | '' 56 | ] 57 | ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/data/Servicerenderv1Test/body-test.html: -------------------------------------------------------------------------------- 1 | 2 | ## Test multiple BODY functions ## 3 | 4 |
    5 |
    6 | Markfown *should* not be rendered in BODY 7 | 8 |
    9 | 10 | 11 | 12 | Content of HEADER element should not be printed 13 | 14 | 15 | 16 |
    17 | 18 |
    19 | 20 | 21 | 34 | 35 | 36 |
    37 | 38 | 39 | 46 | 47 | 48 | 49 | 50 |
    51 |
    52 | 53 |

    Titles should be transformet to links

    54 | 55 |

    my first title

    56 | 57 |

    my second title

    58 | 59 |

    This is another title again and again, but this one is loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooonnnnnnnnger with lot of words

    60 | 61 |

    Dû tëxte encodé àveç dès châräctères nôn ASCII

    62 | 63 |

    イリノイ州シカゴにて、アイルランド系の家庭に、9

    64 | 65 |
    66 | 67 | 68 |
    69 | 70 | 71 |
    72 |

    heading 01

    73 | 74 |

    should have no ID

    75 | 76 |

    heading 02

    77 | 78 |

    should have an ID

    79 | 80 |

    heading 03

    81 | 82 |

    should have an ID

    83 | 84 |

    heading 04

    85 | 86 |

    should have an ID

    87 | 88 |
    heading 05
    89 | 90 |

    should have an ID

    91 | 92 |
    heading 06
    93 | 94 |

    should have no ID

    95 | 96 |
    97 | 98 | 99 |
    100 | 101 |

    This should not work

    102 | 103 | %HEADER % 104 | 105 | %FDS% 106 | 107 | -------------------------------------------------------------------------------- /tests/data/Servicerenderv1Test/body-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "body-test", 3 | "title": "body-test", 4 | "description": "", 5 | "lang": "", 6 | "tag": [], 7 | "date": "2022-10-13T22:46:00+0200", 8 | "datecreation": "2022-10-13T22:46:58+0200", 9 | "datemodif": "2022-10-13T23:03:12+0200", 10 | "daterender": "2022-10-13T23:03:14+0200", 11 | "css": "", 12 | "javascript": "", 13 | "body": "## Test multiple BODY functions ##\n\n
    \n
    \nMarkfown *should* not be rendered in BODY\n\n
    \n\n\n \n Content of HEADER element should not be printed\n \n<\/code>\n\n
    \n\n
    \n\n%NAV?markdown=0%\n\n
    \n\n%ASIDE?falsething=0%\n\n\n%MAIN?headeranchor=1%\n\n
    \n\n%FOOTER?headerid=2-5%\n\n
    \n\n

    This should not work<\/h1>\n\n%HEADER %\n\n%FDS%", 14 | "header": "This line should never exist in HTML render !!!!", 15 | "main": "_______________________\n\nTitles should be transformet to links\n\n# my first title\n\n## my second title\n\n## This is another title again and again, but this one is loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooonnnnnnnnger with lot of words\n\nDû tëxte encodé àveç dès châräctères nôn ASCII\n\nイリノイ州シカゴにて、アイルランド系の家庭に、9", 16 | "nav": "This `Nav` Element should have markdown disabled\n
    \n
    \n## This is not a title\n
    \n
    \n[this will stay as a text and will not become a link](not-an url)", 17 | "aside": "```\n\tAside should apear normally with markdown and everything\n```\n\n%HEADER% >>>> should not call `HEADER` Element\n", 18 | "footer": "# heading 01\n\nshould have no ID\n\n## heading 02\n\nshould have an ID\n\n### heading 03\n\nshould have an ID\n\n#### heading 04\n\nshould have an ID\n\n##### heading 05\n\nshould have an ID\n\n###### heading 06\n\nshould have no ID", 19 | "externalcss": [], 20 | "customhead": "", 21 | "secure": 0, 22 | "interface": "main", 23 | "linkto": [], 24 | "templatebody": "", 25 | "templatecss": "", 26 | "templatejavascript": "", 27 | "templateoptions": [ 28 | "thumbnail", 29 | "recursivecss", 30 | "externalcss", 31 | "favicon", 32 | "externaljavascript" 33 | ], 34 | "favicon": "", 35 | "thumbnail": "", 36 | "authors": [ 37 | "vincent" 38 | ], 39 | "displaycount": 16, 40 | "visitcount": 0, 41 | "editcount": 48, 42 | "sleep": 0, 43 | "redirection": "", 44 | "refresh": 0, 45 | "password": null, 46 | "version": 1 47 | } 48 | -------------------------------------------------------------------------------- /tests/data/Servicerenderv1Test/date-time-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 | 6 |
    7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 |
    25 |


    26 |
    27 |
    28 |

    29 | 30 |
    31 | 32 | 33 | 34 |
    35 | 36 | 37 |
    38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/data/Servicerenderv1Test/date-time-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "date-time-test", 3 | "title": "date-time-test", 4 | "description": "", 5 | "lang": "", 6 | "tag": [], 7 | "date": "2022-10-13T19:39:55+0200", 8 | "datecreation": "2022-10-13T19:39:55+0200", 9 | "datemodif": "2022-10-13T19:39:55+0200", 10 | "daterender": "2022-10-13T19:39:55+0200", 11 | "css": "", 12 | "javascript": "", 13 | "body": "%HEADER%\n\n%NAV%\n\n%ASIDE%\n\n%MAIN%\n\n%FOOTER%", 14 | "header": "", 15 | "main": "%DATE%\n%TIME%\n%DATE?format=full&lang=fr%\n%TIME?format=medium&lang=ja%", 16 | "nav": "last modification: %DATEMODIF% at %TIMEMODIF%", 17 | "aside": "", 18 | "footer": "", 19 | "externalcss": [], 20 | "customhead": "", 21 | "secure": 0, 22 | "interface": "main", 23 | "linkto": [], 24 | "templatebody": "", 25 | "templatecss": "", 26 | "templatejavascript": "", 27 | "templateoptions": [ 28 | "externalcss", 29 | "externaljavascript", 30 | "favicon", 31 | "thumbnail", 32 | "recursivecss" 33 | ], 34 | "favicon": "", 35 | "thumbnail": "", 36 | "authors": [ 37 | "vincent" 38 | ], 39 | "displaycount": 0, 40 | "visitcount": 0, 41 | "editcount": 0, 42 | "sleep": 0, 43 | "redirection": "", 44 | "refresh": 0, 45 | "password": null, 46 | "version": 1 47 | } 48 | -------------------------------------------------------------------------------- /tests/data/Servicerenderv1Test/empty-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 | 6 |
    7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 |
    25 | 26 | 27 |
    28 | 29 | 30 | 31 |
    32 | 33 | 34 |
    35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/data/Servicerenderv1Test/empty-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "empty-test", 3 | "title": "empty-test", 4 | "description": "", 5 | "lang": "", 6 | "tag": [], 7 | "date": "2022-10-13T19:39:55+0200", 8 | "datecreation": "2022-10-13T19:39:55+0200", 9 | "datemodif": "2022-10-13T19:39:55+0200", 10 | "daterender": "2022-10-13T19:39:55+0200", 11 | "css": "", 12 | "javascript": "", 13 | "body": "%HEADER%\n\n%NAV%\n\n%ASIDE%\n\n%MAIN%\n\n%FOOTER%", 14 | "header": "", 15 | "main": "", 16 | "nav": "", 17 | "aside": "", 18 | "footer": "", 19 | "externalcss": [], 20 | "customhead": "", 21 | "secure": 0, 22 | "interface": "main", 23 | "linkto": [], 24 | "templatebody": "", 25 | "templatecss": "", 26 | "templatejavascript": "", 27 | "templateoptions": [ 28 | "externalcss", 29 | "externaljavascript", 30 | "favicon", 31 | "thumbnail", 32 | "recursivecss" 33 | ], 34 | "favicon": "", 35 | "thumbnail": "", 36 | "authors": [ 37 | "vincent" 38 | ], 39 | "displaycount": 0, 40 | "visitcount": 0, 41 | "editcount": 0, 42 | "sleep": 0, 43 | "redirection": "", 44 | "refresh": 0, 45 | "password": null, 46 | "version": 1 47 | } 48 | -------------------------------------------------------------------------------- /tests/data/Servicerenderv1Test/external-links-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 | 6 |
    7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 |
    25 |

    https://club1.fr

    26 | 27 |

    link to my external life using Michel Fortin's Markdown extra

    28 | 29 |

    https://club1.fr

    30 | 31 |

    test de lien vers CLUB1

    32 | 33 |

    test de lien vers CLUB1

    34 | 35 | 38 | 39 |
    <a href="https://club1.fr" class="total  shell">dans un codeblock</a>
    40 | 
    41 | 42 |
    43 | 44 | 45 | 46 |
    47 | 48 | 49 |
    50 | 51 | 52 | -------------------------------------------------------------------------------- /tests/data/Servicerenderv1Test/external-links-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "external-links-test", 3 | "title": "external-links-test", 4 | "description": "", 5 | "lang": "", 6 | "tag": [], 7 | "date": "2022-12-08T20:17:00+0100", 8 | "datecreation": "2022-12-08T20:17:55+0100", 9 | "datemodif": "2022-12-08T23:58:25+0100", 10 | "daterender": "2022-12-08T23:58:30+0100", 11 | "css": "", 12 | "javascript": "", 13 | "body": "%HEADER%\n\n%NAV%\n\n%ASIDE%\n\n%MAIN%\n\n%FOOTER%", 14 | "header": "", 15 | "main": "\n\n[link to my external life using Michel Fortin's Markdown extra](https:\/\/club1.fr) {.testclass#testid}\n\nhttps:\/\/club1.fr\n\ntest de lien vers CLUB1<\/a>\n\ntest de lien vers CLUB1<\/a>\n\n 15 | 16 |
    <a href="https://club1.fr" class="total  shell">dans un codeblock</a>
    17 | 
    18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/data/Servicerenderv2Test/external-links-test-v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "external-links-test-v2", 3 | "title": "external-links-test-v2", 4 | "description": "", 5 | "lang": "", 6 | "tag": [], 7 | "latitude": null, 8 | "longitude": null, 9 | "date": "2023-08-06T19:33:00+0200", 10 | "datecreation": "2023-08-06T19:33:34+0200", 11 | "datemodif": "2023-08-06T19:33:37+0200", 12 | "daterender": "2023-08-06T19:33:38+0200", 13 | "css": "", 14 | "javascript": "", 15 | "body": "%CONTENT%", 16 | "externalcss": [], 17 | "customhead": "", 18 | "secure": 0, 19 | "interface": "content", 20 | "linkto": [], 21 | "templatebody": "", 22 | "templatecss": "", 23 | "templatejavascript": "", 24 | "templateoptions": [ 25 | "thumbnail", 26 | "recursivecss", 27 | "externalcss", 28 | "favicon", 29 | "externaljavascript" 30 | ], 31 | "favicon": "", 32 | "thumbnail": "", 33 | "authors": [ 34 | "vincent" 35 | ], 36 | "displaycount": 1, 37 | "visitcount": 0, 38 | "editcount": 1, 39 | "sleep": 0, 40 | "redirection": "", 41 | "refresh": 0, 42 | "password": null, 43 | "postprocessaction": false, 44 | "version": 2, 45 | "content": "\r\n\r\n[link to my external life using Michel Fortin's Markdown extra](https:\/\/club1.fr) {.testclass#testid}\r\n\r\nhttps:\/\/club1.fr\r\n\r\ntest de lien vers CLUB1<\/a>\r\n\r\ntest de lien vers CLUB1<\/a>\r\n\r\n