Repository: GetPublii/Publii Branch: master Commit: 1ea7e78bd4ec Files: 777 Total size: 20.8 MB Directory structure: gitextract__icctd57/ ├── .editorconfig ├── .github/ │ ├── DISCUSSION_TEMPLATE/ │ │ └── bug-report.yml │ ├── FUNDING.yml │ └── ISSUE_TEMPLATE/ │ ├── bug.yml │ ├── config.yml │ └── feature.yml ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── app/ │ ├── back-end/ │ │ ├── app-preload.js │ │ ├── app.js │ │ ├── author.js │ │ ├── authors.js │ │ ├── builddata.json │ │ ├── events/ │ │ │ ├── _modules.js │ │ │ ├── app.js │ │ │ ├── author.js │ │ │ ├── authors.js │ │ │ ├── backup.js │ │ │ ├── content.js │ │ │ ├── credits.js │ │ │ ├── deploy.js │ │ │ ├── file-manager.js │ │ │ ├── image-uploader.js │ │ │ ├── import.js │ │ │ ├── menu.js │ │ │ ├── notifications.js │ │ │ ├── page.js │ │ │ ├── plugin.js │ │ │ ├── plugins-api.js │ │ │ ├── post.js │ │ │ ├── preview.js │ │ │ ├── site.js │ │ │ ├── sync.js │ │ │ ├── tag.js │ │ │ └── tags.js │ │ ├── helpers/ │ │ │ ├── app-files.js │ │ │ ├── avatar.js │ │ │ ├── context-menu-builder.js │ │ │ ├── db.utils.js │ │ │ ├── file.js │ │ │ ├── image.helper.js │ │ │ ├── slug.js │ │ │ ├── specs/ │ │ │ │ ├── avatar.spec.js │ │ │ │ └── slug.spec.js │ │ │ ├── updates.helper.js │ │ │ ├── utils.js │ │ │ └── validators/ │ │ │ ├── language-config.js │ │ │ └── plugin-config.js │ │ ├── image.js │ │ ├── languages.js │ │ ├── migrators/ │ │ │ └── site-config.js │ │ ├── model.js │ │ ├── modules/ │ │ │ ├── backup/ │ │ │ │ ├── backup.js │ │ │ │ └── create-from-backup.js │ │ │ ├── custom-changes/ │ │ │ │ └── ftp/ │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── TODO │ │ │ │ ├── lib/ │ │ │ │ │ ├── connection.js │ │ │ │ │ └── parser.js │ │ │ │ └── package.json │ │ │ ├── deploy/ │ │ │ │ ├── deployment.js │ │ │ │ ├── ftp-alt.js │ │ │ │ ├── ftp.js │ │ │ │ ├── git.js │ │ │ │ ├── github-pages.js │ │ │ │ ├── gitlab-pages.js │ │ │ │ ├── google-cloud.js │ │ │ │ ├── libraries/ │ │ │ │ │ └── netlify-api.js │ │ │ │ ├── manual.js │ │ │ │ ├── netlify.js │ │ │ │ ├── s3.js │ │ │ │ └── sftp.js │ │ │ ├── import/ │ │ │ │ ├── automatic-paragraphs.js │ │ │ │ ├── import.js │ │ │ │ └── wxr-parser.js │ │ │ ├── plugins/ │ │ │ │ ├── plugins-api.js │ │ │ │ └── plugins-helpers.js │ │ │ └── render-html/ │ │ │ ├── contexts/ │ │ │ │ ├── 404.js │ │ │ │ ├── author.js │ │ │ │ ├── feed.js │ │ │ │ ├── home.js │ │ │ │ ├── page-preview.js │ │ │ │ ├── page.js │ │ │ │ ├── post-preview.js │ │ │ │ ├── post.js │ │ │ │ ├── search.js │ │ │ │ ├── tag.js │ │ │ │ └── tags.js │ │ │ ├── handlebars/ │ │ │ │ └── helpers/ │ │ │ │ ├── _modules.js │ │ │ │ ├── asset.js │ │ │ │ ├── canonical-link.js │ │ │ │ ├── check-if-all.js │ │ │ │ ├── check-if-any.js │ │ │ │ ├── check-if-none.js │ │ │ │ ├── check-if.js │ │ │ │ ├── concatenate.js │ │ │ │ ├── contains.js │ │ │ │ ├── css.js │ │ │ │ ├── date.js │ │ │ │ ├── encode-url-fragment.js │ │ │ │ ├── encode-url.js │ │ │ │ ├── feed-link.js │ │ │ │ ├── font.js │ │ │ │ ├── gdpr-script-blocker.js │ │ │ │ ├── get-author.js │ │ │ │ ├── get-authors.js │ │ │ │ ├── get-page.js │ │ │ │ ├── get-pages-by-custom-field.js │ │ │ │ ├── get-pages.js │ │ │ │ ├── get-post-by-tags.js │ │ │ │ ├── get-post.js │ │ │ │ ├── get-posts-by-custom-field.js │ │ │ │ ├── get-posts-by-tags.js │ │ │ │ ├── get-posts.js │ │ │ │ ├── get-tag.js │ │ │ │ ├── get-tags.js │ │ │ │ ├── image-dimensions.js │ │ │ │ ├── is-current-page.js │ │ │ │ ├── is-empty.js │ │ │ │ ├── is-not-empty.js │ │ │ │ ├── is-not.js │ │ │ │ ├── is.js │ │ │ │ ├── join.js │ │ │ │ ├── js.js │ │ │ │ ├── json-ld.js │ │ │ │ ├── jsonify.js │ │ │ │ ├── lazyload.js │ │ │ │ ├── math.js │ │ │ │ ├── menu-item-classes.js │ │ │ │ ├── menu-url.js │ │ │ │ ├── meta-description.js │ │ │ │ ├── meta-robots.js │ │ │ │ ├── not-contains.js │ │ │ │ ├── orderby.js │ │ │ │ ├── page-url.js │ │ │ │ ├── publii-footer.js │ │ │ │ ├── publii-head.js │ │ │ │ ├── responsive-image-attributes.js │ │ │ │ ├── responsive-sizes.js │ │ │ │ ├── responsive-srcset.js │ │ │ │ ├── reverse.js │ │ │ │ ├── social-meta-tags.js │ │ │ │ ├── specs/ │ │ │ │ │ ├── check-if-all.spec.js │ │ │ │ │ ├── check-if-any.spec.js │ │ │ │ │ ├── check-if-none.spec.js │ │ │ │ │ ├── check-if.spec.js │ │ │ │ │ ├── feed-link.spec.js │ │ │ │ │ ├── font.spec.js │ │ │ │ │ ├── is-empty.spec.js │ │ │ │ │ ├── is-not-empty.spec.js │ │ │ │ │ ├── jsonify.spec.js │ │ │ │ │ └── translate.spec.js │ │ │ │ └── translate.js │ │ │ ├── helpers/ │ │ │ │ ├── content.js │ │ │ │ ├── deleteEmpty.js │ │ │ │ ├── diffCopy.js │ │ │ │ ├── files.js │ │ │ │ ├── gdpr.js │ │ │ │ ├── helpers.js │ │ │ │ ├── sitemap.js │ │ │ │ ├── specs/ │ │ │ │ │ └── url.spec.js │ │ │ │ ├── template.js │ │ │ │ ├── url.js │ │ │ │ └── view-settings.js │ │ │ ├── items/ │ │ │ │ ├── author.js │ │ │ │ ├── featured-image.js │ │ │ │ ├── page.js │ │ │ │ ├── post.js │ │ │ │ └── tag.js │ │ │ ├── renderer-cache.js │ │ │ ├── renderer-context.js │ │ │ ├── renderer-plugins.js │ │ │ ├── renderer.js │ │ │ ├── text-renderers/ │ │ │ │ ├── blockeditor.js │ │ │ │ └── markdown.js │ │ │ └── validators/ │ │ │ └── theme-config.js │ │ ├── page.js │ │ ├── pages.js │ │ ├── plugins.js │ │ ├── post.js │ │ ├── posts.js │ │ ├── site.js │ │ ├── sites.js │ │ ├── sql/ │ │ │ └── 1.0.0.sql │ │ ├── tag.js │ │ ├── tags.js │ │ ├── themes.js │ │ ├── vendor/ │ │ │ └── locutus/ │ │ │ └── htmlspecialchars.js │ │ ├── window-manager.js │ │ └── workers/ │ │ ├── backup/ │ │ │ ├── create.js │ │ │ └── restore.js │ │ ├── deploy/ │ │ │ └── deployment.js │ │ ├── import/ │ │ │ ├── check.js │ │ │ └── import.js │ │ ├── renderer/ │ │ │ └── preview.js │ │ └── thumbnails/ │ │ ├── post-images.js │ │ └── regenerate.js │ ├── build/ │ │ └── config.gypi │ ├── config/ │ │ ├── AST.app.config.js │ │ └── AST.currentSite.config.js │ ├── default-files/ │ │ ├── default-languages/ │ │ │ ├── en-gb/ │ │ │ │ ├── config.json │ │ │ │ ├── translations.json │ │ │ │ └── wysiwyg.json │ │ │ └── pl/ │ │ │ ├── config.json │ │ │ ├── translations.json │ │ │ └── wysiwyg.json │ │ ├── default-themes/ │ │ │ └── simple/ │ │ │ ├── 404.hbs │ │ │ ├── CHANGELOG.md │ │ │ ├── assets/ │ │ │ │ ├── css/ │ │ │ │ │ ├── editor.css │ │ │ │ │ ├── main.css │ │ │ │ │ └── style.css │ │ │ │ ├── dynamic/ │ │ │ │ │ └── fonts/ │ │ │ │ │ ├── adventpro/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── albertsans/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── aleo/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── andadapro/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── antonio/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── archivonarrow/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── asap/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── assistant/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── besley/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── bigshouldersdisplay/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── bitcount/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── bitter/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── bodonimoda/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── brygada1918/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── cabin/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── cairo/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── cinzel/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── comfortaa/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── comme/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── dancingscript/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── danfo/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── dmsans/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── domine/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── dosis/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── doto/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── dynapuff/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── exo/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── familjengrotesk/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── faustina/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── figtree/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── finlandica/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── frankruhllibre/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── fredoka/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── funneldisplay/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── gantari/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── geistmono/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── glory/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── gluten/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── googlesanscode/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── grenzegotisch/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── handjet/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── heebo/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── hostgrotesk/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── imbue/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── inclusivesans/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── instrumentsans/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── jetbrainsmono/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── jura/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── kalnia/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── karla/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── kreon/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── kumbhsans/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── labrada/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── leaguespartan/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── lemonada/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── lexend/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── lexenddeca/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── librefranklin/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── lora/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── manrope/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── manuale/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── mavenpro/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── merriweathersans/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── montserrat/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── mulish/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── nunito/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── orbitron/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── oswald/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── outfit/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── oxanium/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── parkinsans/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── petrona/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── playfairdisplay/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── playwriteusmodern/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── playwriteustrad/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── plusjakartasans/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── pontanosans/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── publicsans/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── quicksand/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── radiocanadabig/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── raleway/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── redhatdisplay/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── redhatmono/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── redhattext/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── redrose/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── rem/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── robotoflex/ │ │ │ │ │ │ └── LICENSE.txt │ │ │ │ │ ├── robotoslab/ │ │ │ │ │ │ └── LICENSE.txt │ │ │ │ │ ├── rokkitt/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── rubik/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── ruda/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── smoochsans/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── sora/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── sourcecodepro/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── spartan/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── sticknobills/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── susemono/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── teachers/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── tektur/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── tourney/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── urbanist/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── varta/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── victormono/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── wixmadefortext/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── workbench/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── worksans/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── yanonekaffeesatz/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── yrsa/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ ├── zalandosans/ │ │ │ │ │ │ └── OFL.txt │ │ │ │ │ └── zalandosansexpanded/ │ │ │ │ │ └── OFL.txt │ │ │ │ └── js/ │ │ │ │ ├── scripts.js │ │ │ │ ├── svg-fix.js │ │ │ │ └── svg-map.js │ │ │ ├── author.hbs │ │ │ ├── computed-options.js │ │ │ ├── config.json │ │ │ ├── dynamic-assets-mapping.js │ │ │ ├── index.hbs │ │ │ ├── page-empty.hbs │ │ │ ├── page.hbs │ │ │ ├── partials/ │ │ │ │ ├── fonts.hbs │ │ │ │ ├── footer.hbs │ │ │ │ ├── head.hbs │ │ │ │ ├── menu.hbs │ │ │ │ ├── navbar.hbs │ │ │ │ ├── pagination.hbs │ │ │ │ ├── share-buttons.hbs │ │ │ │ ├── simple-menu.hbs │ │ │ │ └── subpages-list.hbs │ │ │ ├── post.hbs │ │ │ ├── posts.hbs │ │ │ ├── search.hbs │ │ │ ├── simple.lang.json │ │ │ ├── tag.hbs │ │ │ ├── tags.hbs │ │ │ ├── theme-variables.js │ │ │ └── visual-override.js │ │ ├── gdpr-assets/ │ │ │ ├── gdpr.css │ │ │ ├── gdpr.js │ │ │ └── template.html │ │ ├── theme-files/ │ │ │ ├── config.json │ │ │ ├── feed-json.hbs │ │ │ ├── feed-xml.hbs │ │ │ ├── menu.hbs │ │ │ ├── pagination.hbs │ │ │ └── sitemap.xsl │ │ └── vendor/ │ │ └── prism.js │ ├── dist/ │ │ └── index.html │ ├── license.txt │ ├── licenses/ │ │ ├── LICENSES.chromium.html │ │ ├── agent-base/ │ │ │ └── license.txt │ │ ├── all-licenses.json │ │ ├── assert-plus/ │ │ │ └── license.txt │ │ ├── base64url/ │ │ │ └── license.txt │ │ ├── bindings/ │ │ │ └── license.txt │ │ ├── brace-expansion/ │ │ │ └── license.txt │ │ ├── buffer-alloc/ │ │ │ └── license.txt │ │ ├── buffer-alloc-unsafe/ │ │ │ └── license.txt │ │ ├── buffer-equal/ │ │ │ └── license.txt │ │ ├── buffer-fill/ │ │ │ └── license.txt │ │ ├── buffer-from/ │ │ │ └── license.txt │ │ ├── bufferjs/ │ │ │ └── license.txt │ │ ├── capture-stack-trace/ │ │ │ └── license.txt │ │ ├── chainsaw/ │ │ │ └── license.txt │ │ ├── cli/ │ │ │ └── license.txt │ │ ├── clipboard/ │ │ │ └── license.txt │ │ ├── codemirror/ │ │ │ └── license.txt │ │ ├── colors/ │ │ │ └── license.txt │ │ ├── commander/ │ │ │ └── license.txt │ │ ├── deep-equal/ │ │ │ └── license.txt │ │ ├── devtron/ │ │ │ └── license.txt │ │ ├── diff/ │ │ │ └── license.txt │ │ ├── electron/ │ │ │ └── license.txt │ │ ├── end-of-stream/ │ │ │ └── license.txt │ │ ├── es6-promisify/ │ │ │ └── license.txt │ │ ├── feathericons/ │ │ │ └── license.txt │ │ ├── follow-redirects/ │ │ │ └── license.txt │ │ ├── fresh/ │ │ │ └── license.txt │ │ ├── generate-function/ │ │ │ └── license.txt │ │ ├── get-stdin/ │ │ │ └── license.txt │ │ ├── growl/ │ │ │ └── license.txt │ │ ├── handlebars/ │ │ │ └── license.txt │ │ ├── has/ │ │ │ └── license.txt │ │ ├── he/ │ │ │ └── license.txt │ │ ├── https-proxy-agent/ │ │ │ └── license.txt │ │ ├── humps/ │ │ │ └── license.txt │ │ ├── ignore/ │ │ │ └── license.txt │ │ ├── imurmurhash/ │ │ │ └── license.txt │ │ ├── invert-kv/ │ │ │ └── license.txt │ │ ├── isarray/ │ │ │ └── license.txt │ │ ├── jquery/ │ │ │ └── license.txt │ │ ├── jquery-ui/ │ │ │ └── license.txt │ │ ├── json-schema/ │ │ │ └── license.txt │ │ ├── lazystream/ │ │ │ └── license.txt │ │ ├── libvips/ │ │ │ └── license.txt │ │ ├── licenses.json │ │ ├── locutus/ │ │ │ └── license.txt │ │ ├── log-driver/ │ │ │ └── license.txt │ │ ├── lucide/ │ │ │ └── license.txt │ │ ├── mocha/ │ │ │ └── license.txt │ │ ├── ncname/ │ │ │ └── license.txt │ │ ├── nested-sortable/ │ │ │ └── license.txt │ │ ├── node-slug/ │ │ │ └── license.txt │ │ ├── normalize.css/ │ │ │ └── license.txt │ │ ├── parse-bmfont-ascii/ │ │ │ └── license.txt │ │ ├── parse-bmfont-xml/ │ │ │ └── license.txt │ │ ├── punycode/ │ │ │ └── license.txt │ │ ├── range-parser/ │ │ │ └── license.txt │ │ ├── read-chunk/ │ │ │ └── license.txt │ │ ├── select2/ │ │ │ └── license.txt │ │ ├── send/ │ │ │ └── license.txt │ │ ├── slash/ │ │ │ └── license.txt │ │ ├── sortablejs/ │ │ │ └── license.txt │ │ ├── source-map/ │ │ │ └── license.txt │ │ ├── stream-consume/ │ │ │ └── license.txt │ │ ├── stream-events/ │ │ │ └── license.txt │ │ ├── stream-to/ │ │ │ └── license.txt │ │ ├── stream-to-buffer/ │ │ │ └── license.txt │ │ ├── stubs/ │ │ │ └── license.txt │ │ ├── tabler-icons/ │ │ │ └── license.txt │ │ ├── tinycolorpicker/ │ │ │ └── license.txt │ │ ├── tinymce/ │ │ │ └── license.txt │ │ ├── tootallnate/ │ │ │ └── once/ │ │ │ └── license.txt │ │ ├── topo/ │ │ │ └── license.txt │ │ ├── tr46/ │ │ │ └── license.txt │ │ ├── trim/ │ │ │ └── license.txt │ │ ├── typo-js/ │ │ │ └── license.txt │ │ ├── uri-js/ │ │ │ └── license.txt │ │ ├── vendor-licenses.json │ │ ├── window-size/ │ │ │ └── license.txt │ │ ├── wordwrap/ │ │ │ └── license.txt │ │ ├── xml-char-classes/ │ │ │ └── license.txt │ │ ├── xml-parse-from-string/ │ │ │ └── license.txt │ │ ├── xml2json/ │ │ │ └── license.txt │ │ ├── xmlbuilder/ │ │ │ └── license.txt │ │ └── xregexp/ │ │ └── license.txt │ ├── main.js │ ├── package.json │ └── src/ │ ├── assets/ │ │ └── vendor/ │ │ ├── css/ │ │ │ ├── codemirror.css │ │ │ └── normalize.css │ │ └── js/ │ │ └── codemirror/ │ │ ├── addon/ │ │ │ ├── display/ │ │ │ │ ├── autorefresh.js │ │ │ │ ├── fullscreen.css │ │ │ │ ├── fullscreen.js │ │ │ │ ├── panel.js │ │ │ │ ├── placeholder.js │ │ │ │ └── rulers.js │ │ │ ├── scroll/ │ │ │ │ ├── annotatescrollbar.js │ │ │ │ ├── scrollpastend.js │ │ │ │ ├── simplescrollbars.css │ │ │ │ └── simplescrollbars.js │ │ │ └── search/ │ │ │ ├── jump-to-line.js │ │ │ ├── match-highlighter.js │ │ │ ├── matchesonscrollbar.css │ │ │ ├── matchesonscrollbar.js │ │ │ ├── search.js │ │ │ └── searchcursor.js │ │ ├── autorefresh.js │ │ ├── codemirror.js │ │ ├── css.js │ │ └── xml.js │ ├── components/ │ │ ├── About.vue │ │ ├── AboutCredits.vue │ │ ├── AboutCreditsList.vue │ │ ├── App.vue │ │ ├── AppLanguages.vue │ │ ├── AppPlugins.vue │ │ ├── AppSettings.vue │ │ ├── AppThemes.vue │ │ ├── AuthorForm.vue │ │ ├── Authors.vue │ │ ├── Backups.vue │ │ ├── CustomCss.vue │ │ ├── CustomHtml.vue │ │ ├── ErrorPopup.vue │ │ ├── FileManager.vue │ │ ├── LanguagesList.vue │ │ ├── LanguagesListItem.vue │ │ ├── LogViewer.vue │ │ ├── MenuItem.vue │ │ ├── MenuItemEditor.vue │ │ ├── MenuPositionPopup.vue │ │ ├── Menus.vue │ │ ├── Message.vue │ │ ├── NotificationsCenter.vue │ │ ├── Pages.vue │ │ ├── PluginsList.vue │ │ ├── PluginsListItem.vue │ │ ├── PostEditorBlockEditor.vue │ │ ├── PostEditorMarkdown.vue │ │ ├── PostEditorTinyMCE.vue │ │ ├── Posts.vue │ │ ├── RegenerateThumbnails.vue │ │ ├── RegenerateThumbnailsPopup.vue │ │ ├── RenderingPopup.vue │ │ ├── ServerSettings.vue │ │ ├── Settings.vue │ │ ├── Sidebar.vue │ │ ├── SidebarMenu.vue │ │ ├── SidebarSites.vue │ │ ├── SidebarSyncButton.vue │ │ ├── Site.vue │ │ ├── SiteAddForm.vue │ │ ├── SiteLogo.vue │ │ ├── SitesList.vue │ │ ├── SitesListItem.vue │ │ ├── SitesPopup.vue │ │ ├── SitesSearch.vue │ │ ├── Splashscreen.vue │ │ ├── SyncPopup.vue │ │ ├── TagForm.vue │ │ ├── Tags.vue │ │ ├── ThemeSettings.vue │ │ ├── ThemesList.vue │ │ ├── ThemesListItem.vue │ │ ├── Tools.vue │ │ ├── ToolsPlugin.vue │ │ ├── TopBar.vue │ │ ├── TopBarAppBar.vue │ │ ├── TopBarDropDown.vue │ │ ├── TopBarDropDownItem.vue │ │ ├── WPImport.vue │ │ ├── WPImportStats.vue │ │ ├── basic-elements/ │ │ │ ├── Alert.vue │ │ │ ├── AuthorsDropDown.vue │ │ │ ├── Button.vue │ │ │ ├── ButtonDropdown.vue │ │ │ ├── CharCounter.vue │ │ │ ├── Checkbox.vue │ │ │ ├── CodeMirrorEditor.vue │ │ │ ├── Collection.vue │ │ │ ├── CollectionCell.vue │ │ │ ├── CollectionHeader.vue │ │ │ ├── CollectionRow.vue │ │ │ ├── ColorPicker.vue │ │ │ ├── Confirm.vue │ │ │ ├── DirSelect.vue │ │ │ ├── Dropdown.vue │ │ │ ├── EmbedConsentsGroups.vue │ │ │ ├── EmptyState.vue │ │ │ ├── Field.vue │ │ │ ├── FieldsGroup.vue │ │ │ ├── FileSelect.vue │ │ │ ├── Footer.vue │ │ │ ├── GConsentModeGroups.vue │ │ │ ├── GdprGroups.vue │ │ │ ├── Header.vue │ │ │ ├── HeaderSearch.vue │ │ │ ├── Icon.vue │ │ │ ├── ImageUpload.vue │ │ │ ├── LogoCreator.vue │ │ │ ├── Overlay.vue │ │ │ ├── PagesDropDown.vue │ │ │ ├── PostsDropDown.vue │ │ │ ├── ProgressBar.vue │ │ │ ├── RadioButton.vue │ │ │ ├── RangeSlider.vue │ │ │ ├── Repeater.vue │ │ │ ├── Separator.vue │ │ │ ├── SmallImageUpload.vue │ │ │ ├── SupportedFeaturesCheck.vue │ │ │ ├── Switcher.vue │ │ │ ├── Tabs.vue │ │ │ ├── TagsDropDown.vue │ │ │ ├── TextArea.vue │ │ │ ├── TextInput.vue │ │ │ └── ThemesDropdown.vue │ │ ├── block-editor/ │ │ │ ├── PubliiBlockEditor.vue │ │ │ ├── assets/ │ │ │ │ ├── prism-theme.scss │ │ │ │ └── typography.scss │ │ │ ├── available-blocks.json │ │ │ ├── blocks-mapping.js │ │ │ ├── components/ │ │ │ │ ├── Block.vue │ │ │ │ ├── BlockAdvancedConfig.vue │ │ │ │ ├── BlockEditor.vue │ │ │ │ ├── BlockLinkPopup.vue │ │ │ │ ├── BlockWrapper.vue │ │ │ │ ├── BlocksList.vue │ │ │ │ ├── default-blocks/ │ │ │ │ │ ├── publii-code/ │ │ │ │ │ │ ├── block.vue │ │ │ │ │ │ ├── config-form.json │ │ │ │ │ │ ├── conversions.js │ │ │ │ │ │ └── render.js │ │ │ │ │ ├── publii-embed/ │ │ │ │ │ │ ├── block.vue │ │ │ │ │ │ ├── config-form.json │ │ │ │ │ │ ├── embed.js │ │ │ │ │ │ └── render.js │ │ │ │ │ ├── publii-gallery/ │ │ │ │ │ │ ├── block.vue │ │ │ │ │ │ ├── config-form.json │ │ │ │ │ │ └── render.js │ │ │ │ │ ├── publii-header/ │ │ │ │ │ │ ├── block.vue │ │ │ │ │ │ ├── config-form.json │ │ │ │ │ │ ├── conversions.js │ │ │ │ │ │ └── render.js │ │ │ │ │ ├── publii-html/ │ │ │ │ │ │ ├── aspect-ratio.js │ │ │ │ │ │ ├── block.vue │ │ │ │ │ │ ├── config-form.json │ │ │ │ │ │ ├── content-filter.js │ │ │ │ │ │ ├── conversions.js │ │ │ │ │ │ └── render.js │ │ │ │ │ ├── publii-image/ │ │ │ │ │ │ ├── block.vue │ │ │ │ │ │ ├── config-form.json │ │ │ │ │ │ └── render.js │ │ │ │ │ ├── publii-list/ │ │ │ │ │ │ ├── block.vue │ │ │ │ │ │ ├── config-form.json │ │ │ │ │ │ ├── conversions.js │ │ │ │ │ │ └── render.js │ │ │ │ │ ├── publii-paragraph/ │ │ │ │ │ │ ├── block.vue │ │ │ │ │ │ ├── config-form.json │ │ │ │ │ │ ├── conversions.js │ │ │ │ │ │ └── render.js │ │ │ │ │ ├── publii-quote/ │ │ │ │ │ │ ├── block.vue │ │ │ │ │ │ ├── config-form.json │ │ │ │ │ │ ├── conversions.js │ │ │ │ │ │ └── render.js │ │ │ │ │ ├── publii-readmore/ │ │ │ │ │ │ ├── block.vue │ │ │ │ │ │ ├── config-form.json │ │ │ │ │ │ └── render.js │ │ │ │ │ ├── publii-separator/ │ │ │ │ │ │ ├── block.vue │ │ │ │ │ │ ├── config-form.json │ │ │ │ │ │ ├── conversions.js │ │ │ │ │ │ └── render.js │ │ │ │ │ └── publii-toc/ │ │ │ │ │ ├── block.vue │ │ │ │ │ ├── config-form.json │ │ │ │ │ └── render.js │ │ │ │ ├── elements/ │ │ │ │ │ ├── EditorIcon.vue │ │ │ │ │ └── Switcher.vue │ │ │ │ ├── extensions/ │ │ │ │ │ ├── ConversionHelpers.js │ │ │ │ │ ├── ShortcutManager.js │ │ │ │ │ └── UndoManager.js │ │ │ │ ├── helpers/ │ │ │ │ │ ├── ContentEditableImprovements.vue │ │ │ │ │ ├── InlineMenuUI.vue │ │ │ │ │ └── TopMenuUI.vue │ │ │ │ ├── mixins/ │ │ │ │ │ ├── AdvancedConfig.vue │ │ │ │ │ ├── HasPreview.vue │ │ │ │ │ ├── InlineMenu.vue │ │ │ │ │ ├── LinkConfig.vue │ │ │ │ │ └── LinkHelpers.vue │ │ │ │ └── utils/ │ │ │ │ ├── SelectedText.js │ │ │ │ └── Utils.js │ │ │ └── vendors/ │ │ │ ├── _modularscale.scss │ │ │ └── modularscale/ │ │ │ ├── _function.scss │ │ │ ├── _pow.scss │ │ │ ├── _respond.scss │ │ │ ├── _round-px.scss │ │ │ ├── _settings.scss │ │ │ ├── _sort.scss │ │ │ ├── _strip-units.scss │ │ │ ├── _sugar.scss │ │ │ ├── _target.scss │ │ │ └── _vars.scss │ │ ├── configs/ │ │ │ ├── defaultDeploymentSettings.js │ │ │ ├── postEditor.config.js │ │ │ ├── preloaderImages.js │ │ │ ├── s3ACLs.js │ │ │ ├── s3Regions.js │ │ │ └── sidebar-icons.js │ │ ├── mixins/ │ │ │ ├── BackToTools.js │ │ │ ├── CollectionCheckboxes.js │ │ │ ├── GoToLastOpenedWebsite.vue │ │ │ └── PostEditorsCommon.vue │ │ └── post-editor/ │ │ ├── AuthorPopup.vue │ │ ├── CodeMirror/ │ │ │ ├── codemirror-4.inline-attachment.js │ │ │ └── inline-attachment.js │ │ ├── DatePopup.vue │ │ ├── EasyMde.vue │ │ ├── Editor.vue │ │ ├── EditorBridge.js │ │ ├── GalleryPopup.vue │ │ ├── HelpPanelBlockEditor.vue │ │ ├── HelpPanelMarkdown.vue │ │ ├── InlineEditor.vue │ │ ├── ItemHelper.js │ │ ├── LinkPopup.vue │ │ ├── LinkToolbar.vue │ │ ├── SearchPopup.vue │ │ ├── Sidebar.vue │ │ ├── SourceCodeEditor.vue │ │ ├── TopBar.vue │ │ └── WritersPanel.vue │ ├── config/ │ │ └── langs.js │ ├── helpers/ │ │ ├── sass-colors.js │ │ ├── utils.js │ │ ├── vendor/ │ │ │ ├── locutus/ │ │ │ │ ├── strings/ │ │ │ │ │ └── strip_tags.js │ │ │ │ └── xml/ │ │ │ │ ├── index.js │ │ │ │ ├── utf8_decode.js │ │ │ │ └── utf8_encode.js │ │ │ └── tinymce/ │ │ │ ├── icons/ │ │ │ │ └── publii/ │ │ │ │ └── icons.js │ │ │ ├── langs/ │ │ │ │ └── readme.md │ │ │ ├── license.txt │ │ │ ├── plugins/ │ │ │ │ └── emoticons/ │ │ │ │ └── js/ │ │ │ │ ├── emojiimages.js │ │ │ │ └── emojis.js │ │ │ └── tinymce.d.ts │ │ └── version-comparator.js │ ├── main.js │ ├── router/ │ │ └── index.js │ ├── scss/ │ │ ├── codemirror.scss │ │ ├── css-variables.scss │ │ ├── editor/ │ │ │ ├── editor-markdown.scss │ │ │ ├── editor-options.scss │ │ │ ├── editor-overrides.scss │ │ │ ├── editor.scss │ │ │ ├── post-editors-common.scss │ │ │ └── scrollbar.scss │ │ ├── empty-states.scss │ │ ├── forms.scss │ │ ├── global.scss │ │ ├── help-panel-common.scss │ │ ├── mixins.scss │ │ ├── notifications.scss │ │ ├── options-sidebar.scss │ │ ├── popup-common.scss │ │ ├── scope-fix.scss │ │ ├── variables.scss │ │ └── vendor/ │ │ ├── _modularscale.scss │ │ ├── codemirror.css │ │ ├── modularscale/ │ │ │ ├── _function.scss │ │ │ ├── _pow.scss │ │ │ ├── _respond.scss │ │ │ ├── _round-px.scss │ │ │ ├── _settings.scss │ │ │ ├── _sort.scss │ │ │ ├── _strip-units.scss │ │ │ ├── _sugar.scss │ │ │ ├── _target.scss │ │ │ └── _vars.scss │ │ ├── normalize.css │ │ └── vue-multiselect.scss │ └── store/ │ ├── default.state.js │ ├── getters/ │ │ ├── app-version.js │ │ ├── author-templates.js │ │ ├── languages.js │ │ ├── notifications-count.js │ │ ├── notifications-status.js │ │ ├── notifications.js │ │ ├── plugins.js │ │ ├── site-authors.js │ │ ├── site-display-names.js │ │ ├── site-names.js │ │ ├── site-pages.js │ │ ├── site-plugins.js │ │ ├── site-posts.js │ │ ├── site-tags.js │ │ ├── tag-templates.js │ │ ├── theme-select.js │ │ └── themes.js │ ├── helpers/ │ │ ├── mutations.js │ │ ├── page-filter.js │ │ ├── page-get-author.js │ │ ├── post-filter.js │ │ ├── post-get-author.js │ │ └── post-get-tags.js │ └── index.js ├── build/ │ ├── config.gypi │ ├── entitlements.mac.plist │ ├── installation/ │ │ ├── icon.icns │ │ ├── volume-prerelease.icns │ │ └── volume.icns │ ├── installer.nsh │ ├── license_en.txt │ └── scripts/ │ ├── afterPack.js │ └── update-build-number.js ├── gulpfile.js ├── internal-tools/ │ └── loc.js ├── package.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true charset = utf-8 indent_style = space indent_size = 4 ================================================ FILE: .github/DISCUSSION_TEMPLATE/bug-report.yml ================================================ title: "[Bug report] " labels: ["Thank you for reporting a bug!"] body: - type: markdown attributes: value: | Before reporting a bug, please explore the existing topics in our [GitHub Discussions](https://github.com/GetPublii/Publii/discussions). If you can't find a discussion that addresses your concern, feel free to initiate a new discussion. If you encounter issues with premium products purchased on the [Publii Marketplace](https://marketplace.getpublii.com/), please use the dedicated support platform provided on the marketplace for assistance. --- Thank you for reporting a bug. We need some information to assist us in promptly investigating and resolving the issue. - type: input id: os attributes: label: Operating system description: "Which operating system do you use to run the Publii app? Please provide the version as well." placeholder: "macOS Monterey 12.2" validations: required: true - type: input id: publii attributes: label: Publii version description: "Which Publii version do you use?" placeholder: "0.38.3 (build 14239)" validations: required: true - type: dropdown id: issue_type attributes: label: Issue type description: "What does the problem relate to?" options: - Application - Free theme - Free plugin - Something else validations: required: true - type: textarea id: bug-description attributes: label: Bug description description: What happened? validations: required: true - type: textarea id: steps attributes: label: Steps to reproduce description: Which steps do we need to take to reproduce this error? - type: textarea id: logs attributes: label: Relevant log output description: If applicable, provide relevant log output that can be generated with the Publii "Log Viewer" tool. render: shell ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: GetPublii open_collective: publii custom: https://getpublii.com/donate/ ================================================ FILE: .github/ISSUE_TEMPLATE/bug.yml ================================================ name: Bug description: File a bug report title: "[Bug]: " body: - type: markdown attributes: value: | Before opening a bug report, please search for the behavior in the existing issues. If you can't find what you're looking for, then please open a new issue. For questions about Publii functionality, **themes**, **plugins**, or other general queries, please contact our development team via the [community forum](https://forum.getpublii.com/). --- Thank you for taking the time to file a bug report. In order to help us investigate and fix the issue as quickly as possible, we need some information. - type: input id: os attributes: label: Operating system description: "Which operating system do you use to run Publii app? Please provide the version as well." placeholder: "macOS Monterey 12.2" validations: required: true - type: input id: publii attributes: label: Publii version description: "Which Publii version do you use?" placeholder: "0.38.3 (build 14239)" validations: required: true - type: dropdown id: editor attributes: label: Post editor description: If you're reporting a bug with a post editor, please specify which one. options: - WYSIWYG editor - Block editor - Markdown editor validations: required: false - type: textarea id: bug-description attributes: label: Bug description description: What happened? validations: required: true - type: textarea id: steps attributes: label: Steps to reproduce description: Which steps do we need to take to reproduce this error? - type: textarea id: logs attributes: label: Relevant log output description: If applicable, provide relevant log output that can be generated with the Publii "Log Viewer" tool. render: shell ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Check the docs url: https://getpublii.com/docs/ about: 'This repository is for reporting bugs and proposing new features. If you need help getting started with Publii, check out the docs!' - name: Visit the community forum url: https://forum.getpublii.com/ about: 'For questions about Publii functionality, themes, plugins, or other general queries, please contact our development team via the community forum.' ================================================ FILE: .github/ISSUE_TEMPLATE/feature.yml ================================================ name: Feature Request description: Propose a new feature for Publii title: "[Feature Request]: " labels: [feature request] body: - type: markdown attributes: value: | Thanks for proposing a feature for Publii! - type: textarea id: feature-description attributes: label: Feature Description description: How should this feature look like? validations: required: true ================================================ FILE: .gitignore ================================================ *~ .idea .DS_Store .coveralls.yml npm-debug.log /node_modules/ /app/node_modules/ /coverage /app/dist/css /app/dist/vendor /app/dist/*.js /app/dist/*.txt /app/dist/*.js.map /app/dist/*.png /app/dist/*.svg /app/dist/*.node !/app/dist/index.html /dist /cache/ /StaticBlog-darwin-x64/ /StaticBlog-win32-x64/ /Publii-darwin-x64/ /Publii-win32-x64/ app/Publii-win32-x64/ /Publii-win32-ia32/ /Publii-linux-x64/ /Publii-win32-x64-backup/ app/licenses.txt app/Publii-darwin-x64/ /Publii-darwin-x64/ /dmg-release/ /create-dmg/ licenses.txt release/RELEASES *.nupkg release/Setup.exe Publii.dmg release/PubliiSetup.exe *.msi release/Setup.wixobj *.wixpdb release/Setup.wxs .vscode ================================================ FILE: .nvmrc ================================================ v22.18.0 ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ # Publii - Static CMS for privacy-focused, SEO-optimized websites. [![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://github.com/GetPublii/Publii/blob/master/LICENSE) [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/GetPublii/Publii/graphs/commit-activity) [![OpenCollective](https://opencollective.com/publii/backers/badge.svg)](https://opencollective.com/publii/) ![Open Source Love svg1](https://badges.frapsoft.com/os/v1/open-source.svg?v=103) [Publii](https://getpublii.com/) is a desktop-based CMS for Windows, Mac and Linux that makes creating static websites fast and hassle-free, even for beginners. **Current version: 0.47.5 (build 17411)** ## Why Publii? Unlike static-site generators that are often unwieldy and difficult to use, Publii provides an easy-to-understand UI much like server-based CMSs such as WordPress or Joomla!, where users can create posts and other site content, and style their site using a variety of built-in themes and options. Users can enjoy the benefits of a super-fast and secure static website, with all the convenience that a CMS provides. What makes Publii even more unique is that the app runs locally on your desktop rather than on the site's server. Available for Windows, Mac, Linux once the app has been installed you can create a site in minutes, even without internet access; since Publii is a desktop app you can create, update and modify your site offline, then upload the site changes to your server at the click of a button. Publii supports multiple upload options, including standard HTTP/HTTPS servers, Netlify, Amazon S3, GitHub Pages and Google Cloud or SFTP. ![Publii Open Source Static CMS](https://getpublii.com/assets/images/publii-cms.webp) ## Download Publii is available for Mac, Windows, and Linux and can be downloaded from our website: [Download Publii (.exe, .dmg, .deb, .rpm, .AppImage)](https://getpublii.com/download/) ## Developing If you want to build newest version of Publii or contribute to the Publii code, please read about [app build process](https://github.com/GetPublii/Publii/wiki/App-build-process). ## Getting Started You can learn more about getting started in our [User documentation](https://getpublii.com/docs/) or [Developer documentation](https://getpublii.com/dev/). If you have any questions or suggestions, or just need some help with using Publii, you can visit our [Community Hub](https://github.com/GetPublii/Publii/discussions) or follow us on [Twitter](https://twitter.com/GetPublii) ### Learn More * [User docs](https://getpublii.com/docs/) * [Developer docs](https://getpublii.com/dev/) * [Wiki](https://github.com/GetPublii/Publii/wiki/) * [Issues](https://github.com/GetPublii/Publii/issues/) * [Community forum](https://forum.getpublii.com/) ## Contributors This project exists thanks to all the people who contribute. ## Backers Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Publii#backer)] ## Sponsors Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/Publii#sponsor)] ## License Copyright (c) 2026 TidyCustoms. General Public License v3.0, read [LICENSE](https://getpublii.com/license.html) for details. ================================================ FILE: app/back-end/app-preload.js ================================================ const { contextBridge, ipcRenderer, webUtils } = require('electron'); contextBridge.exposeInMainWorld('mainProcessAPI', { shellShowItemInFolder: (url) => ipcRenderer.invoke('publii-shell-show-item-in-folder', url), shellOpenPath: (filePath) => ipcRenderer.invoke('publii-shell-open-path', filePath), shellOpenExternal: (url) => ipcRenderer.invoke('publii-shell-open-external', url), existsSync: (pathToCheck) => ipcRenderer.invoke('publii-native-exists-sync', pathToCheck), normalizePath: (pathToNormalize) => ipcRenderer.invoke('publii-native-normalize-path', pathToNormalize), createMD5: (value) => ipcRenderer.invoke('publii-native-md5', value), getPathForFile: (value) => webUtils.getPathForFile(value), getEnv: () => ({ name: process.env.NODE_ENV, nodeVersion: process.versions.node, chromeVersion: process.versions.chrome, electronVersion: process.versions.electron, platformName: process.platform }), send: (channel, ...data) => { const validChannels = [ 'app-save-color-theme', 'app-license-load', 'app-config-save', 'app-backup-set-location', 'app-theme-upload', 'app-author-save', 'app-author-cancel', 'app-authors-load', 'app-author-delete', 'app-backups-list-load', 'app-backup-remove', 'app-backup-rename', 'app-backup-create', 'app-backup-restore', 'app-site-reload', 'app-site-css-load', 'app-site-css-save', 'app-site-config-save', 'app-site-check-website-to-restore', 'app-site-check-website-catalog-availability', 'app-site-restore-from-backup', 'app-site-remove-temporary-backup-files', 'app-site-restore-from-backup', 'app-file-manager-list', 'app-file-manager-delete', 'app-file-manager-create', 'app-file-manager-upload', 'app-log-files-load', 'app-log-file-load', 'app-menu-update', 'publii-set-spellchecker-language', 'app-post-load', 'app-post-save', 'app-post-cancel', 'app-page-load', 'app-page-save', 'app-page-cancel', 'app-pages-hierarchy-load', 'app-pages-hierarchy-save', 'app-image-upload', 'app-image-upload-remove', 'app-post-delete', 'app-post-duplicate', 'app-post-status-change', 'app-page-delete', 'app-page-duplicate', 'app-page-status-change', 'app-site-regenerate-thumbnails', 'app-site-abort-regenerate-thumbnails', 'app-preview-render', 'app-deploy-test', 'app-site-regenerate-thumbnails-required', 'app-site-switch', 'app-site-create', 'app-site-clone', 'app-site-delete', 'app-license-accept', 'app-deploy-render-abort', 'app-deploy-abort', 'app-deploy-continue', 'app-deploy-render', 'app-deploy-upload', 'app-sync-is-done', 'app-tag-save', 'app-tag-cancel', 'app-tags-load', 'app-tags-status-change', 'app-tag-delete', 'app-site-theme-config-save', 'app-theme-delete', 'app-notifications-retrieve', 'app-wxr-check', 'app-wxr-import', 'app-language-upload', 'app-language-delete', 'app-plugin-upload', 'app-plugin-delete', 'app-site-get-plugins-state', 'app-site-plugin-activate', 'app-site-plugin-deactivate', 'app-site-get-plugin-config', 'app-site-save-plugin-config', 'app-close', 'app-set-ui-zoom-level', 'app-set-notifications-center-state', 'app-get-notifications-file', 'app-pages-hierarchy-update', 'app-content-fields-update' ]; if (validChannels.includes(channel)) { ipcRenderer.send(channel, ...data); } else { console.info('Event: ', channel, ' is not supported in send'); } }, receive: (channel, func) => { const validChannels = [ 'app-data-loaded', 'app-deploy-render-error', 'app-theme-mode:changed', 'app-files-selected', 'app-site-regenerate-thumbnails-progress', 'app-rendering-progress', 'app-deploy-rendered', 'app-connection-in-progress', 'app-connection-error', 'app-connection-success', 'app-uploading-progress', 'app-wxr-import-progress', 'app-show-search-form', 'block-editor-undo', 'block-editor-redo', 'no-remote-files' ]; if (validChannels.includes(channel)) { // Strip event as it includes `sender` ipcRenderer.on(channel, (event, ...args) => func(...args)); } else { console.info('Event: ', channel, ' is not supported in receive'); } }, receiveOnce: (channel, func) => { const validChannels = [ 'app-license-loaded', 'app-config-saved', 'app-file-selected', 'app-theme-uploaded', 'app-author-saved', 'app-authors-loaded', 'app-author-deleted', 'app-backups-list-loaded', 'app-backup-removed', 'app-backup-renamed', 'app-backup-created', 'app-backup-restored', 'app-site-reloaded', 'app-site-css-loaded', 'app-site-css-saved', 'app-site-config-saved', 'app-site-backup-checked', 'app-site-restored-from-backup', 'app-site-website-catalog-availability-checked', 'app-site-restored-from-backup', 'app-file-manager-listed', 'app-file-manager-deleted', 'app-file-manager-created', 'app-file-manager-uploaded', 'app-log-files-loaded', 'app-log-file-loaded', 'app-post-loaded', 'app-post-saved', 'app-post-deleted', 'app-post-duplicated', 'app-post-status-changed', 'app-page-loaded', 'app-page-saved', 'app-page-deleted', 'app-page-duplicated', 'app-page-status-changed', 'app-pages-hierarchy-loaded', 'app-site-regenerate-thumbnails-error', 'app-site-regenerate-thumbnails-success', 'app-preview-rendered', 'app-preview-render-error', 'app-deploy-test-success', 'app-deploy-test-write-error', 'app-deploy-test-error', 'app-site-regenerate-thumbnails-required-status', 'app-site-switched', 'app-site-creation-error', 'app-site-creation-duplicate', 'app-site-creation-db-error', 'app-site-created', 'app-site-cloned', 'app-site-deleted', 'app-license-accepted', 'app-deploy-aborted', 'app-deploy-uploaded', 'app-sync-is-done-saved', 'app-tag-saved', 'app-tags-loaded', 'app-tags-status-changed', 'app-tag-deleted', 'app-site-theme-config-saved', 'app-theme-deleted', 'app-notifications-retrieved', 'app-wxr-imported', 'app-wxr-checked', 'app-directory-selected', 'app-image-uploaded', 'app-files-selected', 'app-language-uploaded', 'app-language-deleted', 'app-plugin-uploaded', 'app-plugin-deleted', 'app-site-plugin-config-saved', 'app-site-plugins-state-loaded', 'app-site-plugin-activated', 'app-site-plugin-deactivated', 'app-site-get-plugin-config-retrieved', 'app-content-fields-updated' ]; if (validChannels.includes(channel)) { // Strip event as it includes `sender` ipcRenderer.once(channel, (event, ...args) => func(...args)); } else { console.info('Event: ', channel, ' is not supported in receiveOnce'); } }, invoke: (command, ...data) => { const validCommands = [ 'app-theme-mode:set-light', 'app-theme-mode:set-dark', 'app-theme-mode:get-theme', 'app-theme-mode:set-system', 'app-credits-list:get-app-path', 'app-main-process-is-osx11-or-higher', 'app-main-process-select-file', 'app-main-process-create-slug', 'app-main-process-select-files', 'publii-get-spellchecker-language', 'app-main-get-spellchecker-languages', 'app-main-set-spellchecker-language-for-webview', 'app-main-process-load-password', 'app-window:minimize', 'app-window:maximize', 'app-window:unmaximize', 'app-window:close', 'app-main-process-select-directory', 'app-main-webview-search-find-in-page', 'app-main-webview-search-stop-find-in-page', 'app-main-load-language', 'app-plugins-api:save-config-file', 'app-plugins-api:save-language-file', 'app-plugins-api:read-config-file', 'app-plugins-api:read-language-file', 'app-plugins-api:read-theme-file', 'app-plugins-api:delete-config-file', 'app-plugins-api:delete-language-file' ]; if (validCommands.includes(command)) { return ipcRenderer.invoke(command, ...data); } else { console.info('Event: ', channel, ' is not supported in invoke'); } return false; }, stopReceive: (channel, func) => { const validChannels = [ 'app-preview-render-error', 'app-connection-error', 'app-wxr-imported', 'app-wxr-import-progress' ]; if (validChannels.includes(channel)) { ipcRenderer.removeListener(channel, func); } else { console.info('Event: ', channel, ' is not supported in stopReceive'); } }, stopReceiveAll: (channel) => { const validChannels = [ 'app-license-accepted', 'app-files-selected', 'app-site-regenerate-thumbnails-error', 'app-site-regenerate-thumbnails-progress', 'app-site-regenerate-thumbnails-success', 'app-preview-render-error', 'app-rendering-progress', 'app-site-created', 'app-site-creation-duplicate', 'app-site-creation-db-error', 'app-site-creation-error', 'app-connection-error', 'app-show-search-form', 'block-editor-undo', 'block-editor-redo' ]; if (validChannels.includes(channel)) { ipcRenderer.removeAllListeners(channel); } else { console.info('Event: ', channel, ' is not supported in stopReceiveAll'); } } }); ================================================ FILE: app/back-end/app.js ================================================ /* * Main Application class */ // Necessary packages const fs = require('fs-extra'); const os = require('os'); const path = require('path'); const Database = os.platform() === 'linux' ? require('node-sqlite3-wasm').Database : require('better-sqlite3'); const compare = require('node-version-compare'); const normalizePath = require('normalize-path'); const url = require('url'); // Electron classes const { screen, shell, nativeTheme, Menu, dialog, BrowserWindow } = require('electron'); // Collection classes const Posts = require('./posts.js'); const Pages = require('./pages.js'); const Tags = require('./tags.js'); const Authors = require('./authors.js'); const Themes = require('./themes.js'); const Languages = require('./languages.js'); const Plugins = require('./plugins.js'); // Helper classes const DBUtils = require('./helpers/db.utils.js'); const Site = require('./site.js'); const Utils = require('./helpers/utils.js'); const FileHelper = require('./helpers/file.js'); // List of the Event classes const EventClasses = require('./events/_modules.js'); // Migration classes const SiteConfigMigrator = require('./migrators/site-config.js'); // Default config const defaultAstAppConfig = require('./../config/AST.app.config'); const defaultAstCurrentSiteConfig = require('./../config/AST.currentSite.config'); // Plugins packages const PluginsAPI = require('./modules/plugins/plugins-api.js') /** * Main app class */ class App { /** * Constructor * * @param startupSettings */ constructor(startupSettings) { this.mainWindow = startupSettings.mainWindow; this.app = startupSettings.app; this.basedir = startupSettings.basedir; this.appDir = path.join(this.app.getPath('documents'), 'Publii'); this.app.appDir = this.appDir; this.initPath = path.join(this.appDir, 'config', 'window-config.json'); this.appConfigPath = path.join(this.appDir, 'config', 'app-config.json'); this.tinymceOverridedConfigPath = path.join(this.appDir, 'config', 'tinymce.override.json'); this.versionData = JSON.parse(FileHelper.readFileSync(__dirname + '/builddata.json', 'utf8')); this.versionData.os = os.platform() === 'darwin' ? 'mac' : os.platform() === 'linux' ? 'linux' : 'win'; this.windowBounds = null; this.appConfig = null; this.tinymceOverridedConfig = {}; this.sites = {}; this.sitesDir = null; this.app.sitesDir = null; this.db = false; this.pluginsAPI = new PluginsAPI(); /* * Run the app */ this.checkDirs(); let loadConfigResult = this.loadConfig(); if (!loadConfigResult) { this.app.quit(); return; } this.loadAdditionalConfig(); this.checkThemes(); let loadingSitesResult = this.loadSites(); if (!loadingSitesResult) { this.app.quit(); return; } this.loadThemes(); this.loadLanguages(); this.loadPlugins(); this.initWindow(); this.initWindowEvents(); } /** * Create the application dir if not exists */ checkDirs() { if (!fs.existsSync(this.appDir)) { fs.mkdirSync(this.appDir); // Create also other dirs fs.mkdirSync(path.join(this.appDir, 'sites')); fs.mkdirSync(path.join(this.appDir, 'config')); fs.mkdirSync(path.join(this.appDir, 'themes')); fs.copySync( path.join(__dirname, '..', 'default-files', 'default-themes').replace('app.asar', 'app.asar.unpacked'), path.join(this.appDir, 'themes'), { filter: this.skipSystemFiles, dereference: true } ); fs.mkdirSync(path.join(this.appDir, 'languages')); fs.mkdirSync(path.join(this.appDir, 'plugins')); } if (!fs.existsSync(path.join(this.appDir, 'backups'))) { fs.mkdirSync(path.join(this.appDir, 'backups')); } if (!fs.existsSync(path.join(this.appDir, 'languages'))) { fs.mkdirSync(path.join(this.appDir, 'languages')); } if (!fs.existsSync(path.join(this.appDir, 'plugins'))) { fs.mkdirSync(path.join(this.appDir, 'plugins')); } } /** * Check if some themes should be updated */ checkThemes() { let appThemesPath = path.join(__dirname, '..', 'default-files', 'default-themes'); let userThemesPath = path.join(this.appDir, 'themes'); // Merge themes directory let appThemeDirs = fs.readdirSync(appThemesPath); for (let file of appThemeDirs) { // Skip files and hidden files if (file.indexOf('.') > -1) { continue; } // Detect missing themes if (!fs.existsSync(path.join(userThemesPath, file))) { fs.mkdirSync(path.join(userThemesPath, file), { recursive: true }); try { fs.copySync( path.join(appThemesPath, file).replace('app.asar', 'app.asar.unpacked'), path.join(userThemesPath, file), { filter: this.skipSystemFiles, dereference: true } ); } catch (err) { fs.appendFile(this.app.getPath('logs') + '/themes-copy-errors.txt', JSON.stringify(err)); } } else { // For existing themes - compare versions let appThemeConfig = path.join(appThemesPath, file, 'config.json'); let userThemeConfig = path.join(userThemesPath, file, 'config.json'); // Check if both config.json files exists if (fs.existsSync(appThemeConfig) && fs.existsSync(userThemeConfig)) { let appThemeData = JSON.parse(FileHelper.readFileSync(appThemeConfig, 'utf8')); let userThemeData = JSON.parse(FileHelper.readFileSync(userThemeConfig, 'utf8')); // If app theme is newer version than the existing one if(compare(appThemeData.version, userThemeData.version) === 1) { // Remove all files from the theme dir fs.emptyDirSync(path.join(userThemesPath, file)); // Copy updated theme files fs.copySync( path.join(appThemesPath, file).replace('app.asar', 'app.asar.unpacked'), path.join(userThemesPath, file), { filter: this.skipSystemFiles, dereference: true } ); } } } } } // Reload website data reloadSite (siteName) { let siteData = this.switchSite(siteName); let siteConfig = this.loadSite(siteName); return { data: siteData, config: siteConfig }; } // Load website and their config and database switchSite (site) { if (!site) { return { status: false }; } const siteDir = path.join(this.sitesDir, site); const menuConfigPath = path.join(siteDir, 'input', 'config', 'menu.config.json'); const themeConfigPath = path.join(siteDir, 'input', 'config', 'theme.config.json'); const dbPath = path.join(siteDir, 'input', 'db.sqlite'); if (!Utils.fileExists(dbPath)) { return { status: false }; } if (this.db) { try { this.db.close(); } catch (e) { console.log('[SWITCH WEBSITE] DB already closed'); } } this.db = new DBUtils(new Database(dbPath)); let tags = new Tags(this, {site}); let posts = new Posts(this, {site}); let pages = new Pages(this, {site}); let authors = new Authors(this, {site}); let themes = new Themes(this, {site}); let themeDir = path.join(siteDir, 'input', 'themes', themes.currentTheme(true)); let themeOverridesDir = path.join(siteDir, 'input', 'themes', themes.currentTheme(true) + '-override'); let themeConfig = Themes.loadThemeConfig(themeConfigPath, themeDir); let menuStructure = FileHelper.readFileSync(menuConfigPath, 'utf8'); let parsedMenuStructure = {}; try { parsedMenuStructure = JSON.parse(menuStructure); } catch (e) { return { status: false }; } return { status: true, posts: posts.load(), pages: pages.load(), tags: tags.load(), authors: authors.load(), postsTags: posts.loadTagsXRef(), postsAuthors: posts.loadAuthorsXRef(), pagesAuthors: pages.loadAuthorsXRef(), postTemplates: themes.loadPostTemplates(), pageTemplates: themes.loadPageTemplates(), tagTemplates: themes.loadTagTemplates(), authorTemplates: themes.loadAuthorTemplates(), themes: themes.load(), themeHasOverrides: Utils.dirExists(themeOverridesDir), themeSettings: themeConfig, menuStructure: parsedMenuStructure, siteDir: siteDir }; } // Load specific website loadSite (siteName) { let dirPath = path.join(this.sitesDir, siteName); let fileStat = fs.statSync(dirPath); // check directories only if (!fileStat.isDirectory()) { return; } // check if the config file exists let configFilePath = path.join(dirPath, 'input', 'config', 'site.config.json'); if (!Utils.fileExists(configFilePath)) { return; } // check if all necessary files exists Site.checkFilesConsistency(this, siteName); // Load the config let defaultSiteConfig = JSON.parse(JSON.stringify(defaultAstCurrentSiteConfig)); let siteConfig = FileHelper.readFileSync(configFilePath); try { siteConfig = JSON.parse(siteConfig); } catch (e) { dialog.showErrorBox('Publii cannot read site config', 'There is an issue with file: ' + configFilePath + "\n\nError details: " + e.message); return; } if (siteConfig.name !== siteName) { siteConfig.name = siteName; fs.writeFileSync(configFilePath, JSON.stringify(siteConfig, null, 4)); } siteConfig = Utils.mergeObjects(defaultSiteConfig, siteConfig); // Migrate old author data if necessary siteConfig = SiteConfigMigrator.moveOldAuthorData(this, siteConfig); // set site data this.sites[siteConfig.name] = JSON.parse(JSON.stringify(siteConfig)); if (this.sites[siteConfig.name].logo.icon.indexOf('#') > -1) { this.sites[siteConfig.name].logo.icon = this.sites[siteConfig.name].logo.icon.split('#')[1]; } // Fill displayName fields for old websites without it if (!this.sites[siteConfig.name].displayName) { this.sites[siteConfig.name].displayName = siteConfig.name; } return siteConfig; } // Load websites loadSites() { if (!fs.existsSync(this.sitesDir)) { dialog.showErrorBox('Publii cannot find your sites folder.', 'Please check if the directory ' + this.sitesDir + ' exists or create it manually, then reopen the application.'); return false; } let files = fs.readdirSync(this.sitesDir); this.sites = {}; for (let siteName of files) { this.loadSite(siteName); } return true; } // Load themes loadThemes() { let themesLoader = new Themes(this); this.themes = themesLoader.loadThemes(); this.themesPath = normalizePath(path.join(this.appDir, 'themes')); this.dirPaths = { sites: normalizePath(path.join(this.appDir, 'sites')), temp: normalizePath(path.join(this.appDir, 'temp')), logs: normalizePath(this.app.getPath('logs')) }; } // Load languages loadLanguages() { let languagesLoader = new Languages(this); this.languages = languagesLoader.loadLanguages(); this.languagesPath = normalizePath(path.join(this.appDir, 'languages')); this.languagesDefaultPath = normalizePath(path.join(__dirname, '..', 'default-files', 'default-languages').replace('app.asar', 'app.asar.unpacked')); this.languageLoadingError = false; if (this.appConfig.language && this.appConfig.languageType) { this.currentLanguageName = this.appConfig.language; this.currentLanguageType = this.appConfig.languageType; this.currentLanguageTranslations = languagesLoader.loadTranslations(this.appConfig.language, this.appConfig.languageType); let languageConfig = languagesLoader.loadLanguageConfig(this.appConfig.language, this.appConfig.languageType); if (languageConfig) { this.currentLanguageMomentLocale = languageConfig.momentLocale; this.currentWysiwygTranslation = languagesLoader.loadWysiwygTranslation(this.appConfig.language, this.appConfig.languageType); } } this.loadDefaultLanguage(languagesLoader, false); } // Load plugins loadPlugins() { let pluginsLoader = new Plugins(this.appDir, this.sitesDir); this.plugins = pluginsLoader.loadPlugins(); this.pluginsPath = normalizePath(path.join(this.appDir, 'plugins')); } // Load default language loadDefaultLanguage (languagesLoader, errorOccurred = false) { this.defaultLanguageName = 'en-gb'; this.defaultLanguageType = 'default'; this.defaultLanguageTranslations = languagesLoader.loadTranslations('en-gb', 'default'); let languageConfig = languagesLoader.loadLanguageConfig('en-gb', 'default'); this.defaultLanguageMomentLocale = languageConfig.momentLocale; this.defaultWysiwygTranslation = languagesLoader.loadWysiwygTranslation('en-gb', 'default'); if (errorOccurred) { this.defaultLanguageLoadingError = true; } } // Load language loadLanguage (lang, type) { if (type !== 'default' && type !== 'installed') { type = 'default'; lang = 'en-gb'; } let languagesLoader = new Languages(this); this.currentLanguageName = lang.replace(/[^a-z\-\_\.]/gmi, ''); this.currentLanguageType = type; this.currentLanguageTranslations = languagesLoader.loadTranslations(lang, type); this.languageLoadingError = false; let languageConfig = languagesLoader.loadLanguageConfig(lang, type); if (languageConfig) { this.currentLanguageMomentLocale = languageConfig.momentLocale; this.currentWysiwygTranslation = languagesLoader.loadWysiwygTranslation(lang, type); } if ( !this.currentLanguageTranslations || !languageConfig || (!this.currentWysiwygTranslation && lang !== 'en-gb') ) { this.languageLoadingError = true; } } // Set language setLanguage (lang, type) { if (type !== 'default' && type !== 'installed') { type = 'default'; lang = 'en-gb'; } this.appConfig.language = lang.replace(/[^a-z\-\_\.]/gmi, ''); this.appConfig.languageType = type; try { fs.writeFileSync(this.appConfigPath, JSON.stringify(this.appConfig, null, 4), {'flags': 'w'}); } catch (e) { if (this.hasPermissionsErrors(e)) { return false; } } return true; } // Read or create the application config loadConfig () { // Try to get window bounds try { this.windowBounds = JSON.parse(FileHelper.readFileSync(this.initPath, 'utf8')); } catch (e) { console.log('The window-config.json file will be created'); } if (!this.windowBounds) { let screens = screen.getAllDisplays(); let width = screens[0].workAreaSize.width; let height = screens[0].workAreaSize.height; for (let i = 0; i < screens.length; i++) { if (screens[i].width < width) { width = screens[i].width; } if (screens[i].height < height) { height = screens[i].height; } } this.windowBounds = { width: width, height: height }; } else { let screens = screen.getAllDisplays(); let isInsideScreenBounds = false; for (let monitor of screens) { if ( this.windowBounds.x >= monitor.bounds.x && this.windowBounds.y >= monitor.bounds.y && this.windowBounds.x + this.windowBounds.width <= monitor.bounds.x + monitor.bounds.width && this.windowBounds.y + this.windowBounds.height <= monitor.bounds.y + monitor.bounds.height ) { isInsideScreenBounds = true; break } } if (!isInsideScreenBounds) { let width = screens[0].workAreaSize.width; let height = screens[0].workAreaSize.height; this.windowBounds = { width: width, height: height }; } } // Try to get application config try { this.appConfig = JSON.parse(FileHelper.readFileSync(this.appConfigPath, 'utf8')); this.appConfig = Utils.mergeObjects(JSON.parse(JSON.stringify(defaultAstAppConfig)), this.appConfig); } catch (e) { if (this.hasPermissionsErrors(e)) { return false; } console.log('The app-config.json file will be created'); this.appConfig = JSON.parse(JSON.stringify(defaultAstAppConfig)); try { fs.writeFileSync(this.appConfigPath, JSON.stringify(this.appConfig, null, 4), {'flags': 'w'}); } catch (e) { if (this.hasPermissionsErrors(e)) { return false; } } return true; } return true; } // Load additional config data loadAdditionalConfig () { // Try to get TinyMCE overrided config try { this.tinymceOverridedConfig = JSON.parse(FileHelper.readFileSync(this.tinymceOverridedConfigPath, 'utf8')); } catch (e) {} if (this.appConfig.sitesLocation) { this.sitesDir = this.appConfig.sitesLocation; this.app.sitesDir = this.appConfig.sitesLocation; } else { this.appConfig.sitesLocation = path.join(this.appDir, 'sites'); this.sitesDir = path.join(this.appDir, 'sites'); this.app.sitesDir = path.join(this.appDir, 'sites'); } this.pluginsHelper = new Plugins(this.appDir, this.sitesDir); } // Check permissions errors hasPermissionsErrors (error) { if (error.code === 'EACCES') { dialog.showErrorBox('Publii has no read/write access to the config folder', 'Please check the permissions of the Publii config folder and try to reopen the application.'); return true; } if (error.code === 'EPERM') { dialog.showErrorBox('Publii has no read/write access to the config folder', 'If you are using macOS 10.15+ - please open "System Preferences", go to "Security & Privacy" and under "Privacy Tab" please check if Publii has proper permissions for the "Files and Documents". For other operating systems - please check the file permissions for the Publii configuration folder.'); return true; } return false; } // Create the window initWindow() { let self = this; let windowParams = this.windowBounds; windowParams.minWidth = 1200; windowParams.minHeight = 700; windowParams.webPreferences = { nodeIntegration: false, contextIsolation: true, spellcheck: true, preload: path.join(__dirname, 'app-preload.js'), icon: path.join(__dirname, 'assets', 'icon.png') }; if (this.appConfig.appTheme === 'dark' || (this.appConfig.appTheme === 'system' && nativeTheme.shouldUseDarkColors)) { windowParams.backgroundColor = '#202128'; } let displays = screen.getAllDisplays(); let externalDisplay = displays.find((display) => { return display.bounds.x !== 0 || display.bounds.y !== 0; }); // Detect case when Publii was displayed on the external display which is now unavailable if ( !externalDisplay && ( windowParams.x < 0 || windowParams.x > screen.getPrimaryDisplay().workAreaSize.width || windowParams.y < 0 || windowParams.y > screen.getPrimaryDisplay().workAreaSize.height ) ) { windowParams.x = 0; windowParams.y = 0; } if((/^darwin/).test(process.platform)) { windowParams.titleBarStyle = 'hidden'; } if((/^win/).test(process.platform)) { windowParams.frame = false; } Menu.setApplicationMenu(null); this.mainWindow = new BrowserWindow(windowParams); this.mainWindow.setMenu(null); this.mainWindow.loadURL('file:///' + this.basedir + '/dist/index.html'); this.mainWindow.removeMenu(); // Register search shortcut listener this.mainWindow.webContents.on('before-input-event', (event, input) => { if (input.type === 'mouseDown' && (input.button === 'back' || input.button === 'forward')) { event.preventDefault(); } if (input.key === 'f' && (input.meta || input.control)) { this.mainWindow.webContents.send('app-show-search-form'); } else if (input.key === 'z' && (input.meta || input.control) && !input.shift) { this.mainWindow.webContents.send('block-editor-undo'); } else if ( (input.key === 'z' && (input.meta || input.control) && input.shift) || (input.key === 'y' && (input.meta || input.control) && !input.shift) ) { this.mainWindow.webContents.send('block-editor-redo'); } }); this.mainWindow.webContents.setWindowOpenHandler(({ url }) => { if (typeof url !== 'string') { return { action: 'deny' }; } let urlToOpen; let allowedProtocols = ['http:', 'https:', 'file:', 'dat:', 'ipfs:', 'dweb:']; try { urlToOpen = new URL(url); } catch (e) { return { action: 'deny' }; } if (allowedProtocols.indexOf(urlToOpen.protocol) > -1) { urlToOpen = urlToOpen.href.replace(/\s/gmi, ''); shell.openExternal(url); } return { action: 'deny' }; }); this.mainWindow.webContents.on('app-command', (e, cmd) => { // disable back/forward mouse buttons if (cmd === 'browser-backward' || cmd === 'browser-forward') { e.preventDefault(); } }); this.mainWindow.webContents.on('did-finish-load', function() { let appData = { version: self.versionData, config: self.appConfig, customConfig: { tinymce: self.tinymceOverridedConfig }, currentLanguage: { name: self.currentLanguageName, translations: self.currentLanguageTranslations, wysiwygTranslation: self.currentWysiwygTranslation, momentLocale: self.currentLanguageMomentLocale, languageLoadingError: self.languageLoadingError }, defaultLanguage: { name: self.defaultLanguageName, translations: self.defaultLanguageTranslations, wysiwygTranslation: self.defaultWysiwygTranslation, momentLocale: self.defaultLanguageMomentLocale, languageLoadingError: self.languageLoadingError }, languages: self.languages, languagesPath: self.languagesPath, languagesDefaultPath: self.languagesDefaultPath, plugins: self.plugins, pluginsPath: self.pluginsPath, sites: self.sites, themes: self.themes, themesPath: self.themesPath, dirs: self.dirPaths, vendorPath: normalizePath(path.join(__dirname, '..', 'default-files', 'vendor').replace('app.asar', 'app.asar.unpacked')) }; self.mainWindow.webContents.send('app-data-loaded', appData); // Open Dev Tools if (self.appConfig.openDevToolsInMain) { self.mainWindow.webContents.openDevTools(); } self.setCurrentZoomLevel(); }); this.mainWindow.on('resize', () => this.setCurrentZoomLevel()); this.mainWindow.on('maximize', () => this.setCurrentZoomLevel()); this.mainWindow.on('unmaximize', () => this.setCurrentZoomLevel()); this.mainWindow.on('restore', () => this.setCurrentZoomLevel()); if (process.platform === 'linux') { this.mainWindow.webContents.on('before-input-event', (event, input) => { if (input.control && input.key === 'q') { this.app.quit(); } }); } // Create context menu const ContextMenuBuilder = require('./helpers/context-menu-builder.js'); let contextMenuBuilder = new ContextMenuBuilder(this.mainWindow.webContents); this.mainWindow.webContents.on('context-menu', (event, params) => { event.preventDefault(); contextMenuBuilder.showPopupMenu(params); }); } // Add events to the window initWindowEvents() { this.mainWindow.on('close', () => { let windowBounds = this.mainWindow.getBounds(); fs.writeFileSync(this.initPath, JSON.stringify(windowBounds, null, 4), {'flags': 'w'}); }); this.mainWindow.on('closed', () => { this.mainWindow = null; }); this.initializeCustomIpcMainEvents(); } // Initializes all custom events for IPC Main thread initializeCustomIpcMainEvents () { // Create instances for all custom event classes let classNames = Object.keys(EventClasses); for (let className of classNames) { new EventClasses[className](this); } } // Getter for the main window object getMainWindow() { return this.mainWindow; } // Function used to filter unnecessary files skipSystemFiles (src, dest) { return src.indexOf('.DS_Store') > -1 ? false : true; } // Function used to add sites to the back-end sites list addSite (siteCatalog, siteData) { this.sites[siteCatalog] = siteData; } // Function used to restore current zoom level of window, because it is lost if zoom is changed after windo load setCurrentZoomLevel () { let zoom = parseFloat(this.appConfig.uiZoomLevel); if (zoom && zoom > 0 && zoom <= 2.5) { this.mainWindow.webContents.setZoomFactor(zoom); } } } module.exports = App; ================================================ FILE: app/back-end/author.js ================================================ const fs = require('fs-extra'); const path = require('path'); const Model = require('./model.js'); const Authors = require('./authors.js'); const Pages = require('./pages.js'); const Posts = require('./posts.js'); const slug = require('./helpers/slug'); const ImageHelper = require('./helpers/image.helper.js'); const Themes = require('./themes.js'); const Utils = require('./helpers/utils.js'); const FileHelper = require('./helpers/file.js'); /** * Author Model - used for operations connected with author management */ class Author extends Model { /** * Creates an instance of the model * * @param appInstance {object} - instance of the application * @param authorData {object} - object with author data */ constructor(appInstance, authorData, storeMode = true) { super(appInstance, authorData); this.id = parseInt(authorData.id, 10); this.authorsData = new Authors(appInstance, authorData); this.postsData = new Posts(appInstance, authorData); this.pagesData = new Pages(appInstance, authorData); this.storeMode = storeMode; if (authorData.additionalData) { this.additionalData = authorData.additionalData; } if (authorData.imageConfigFields) { this.imageConfigFields = authorData.imageConfigFields; } if(authorData.name || authorData.name === '') { this.name = authorData.name; this.username = authorData.username; this.config = authorData.config; this.additionalData = authorData.additionalData; this.prepareAuthorName(); } if (typeof this.additionalData === 'string') { try { this.additionalData = JSON.parse(this.additionalData); } catch (e) { console.log('(!) An issue occurred during initial parsing author additional data', this.id); } } } /** * Saves new/existing author data * * @returns {object} - object with created/edited author data */ save () { if (this.name === '') { return { status: false, message: 'author-empty-name' }; } if (this.username === '' || slug(this.username) === '') { this.username = slug(this.name); } if (slug(this.username).trim() === '') { return { status: false, message: 'author-empty-username' }; } if (!this.isAuthorNameUnique()) { return { status: false, message: 'author-duplicate-name', authors: this.authorsData.load() }; } if (!this.isAuthorUsernameUnique()) { return { status: false, message: 'author-duplicate-username', authors: this.authorsData.load() }; } if (this.id !== 0) { return this.updateAuthor(); } return this.addAuthor(); } /** * Stores new author in the DB * * @returns {{status: boolean, message: string, authors: *}} */ addAuthor() { let sqlQuery = this.db.prepare(`INSERT INTO authors VALUES(null, @name, @slug, '', @config, @additionalData)`); sqlQuery.run({ name: this.name, slug: slug(this.username), config: this.config, additionalData: JSON.stringify(this.additionalData) }); // Get the newly added item ID if necessary if (this.id === 0) { this.id = this.db.prepare('SELECT last_insert_rowid() AS id').get().id; // Move images from the temp directory let tempDirectoryExists = true; let tempImagesDir = path.join(this.siteDir, 'input', 'media', 'authors', 'temp'); try { fs.statSync(tempImagesDir).isDirectory(); } catch (err) { tempDirectoryExists = false; } if (tempDirectoryExists) { let finalImagesDir = path.join(this.siteDir, 'input', 'media', 'authors', (this.id).toString()); fs.copySync(tempImagesDir, finalImagesDir); fs.removeSync(tempImagesDir); } } this.checkAndCleanImages(); return { status: true, message: 'author-added', authorID: this.id, postsAuthors: this.postsData.loadAuthorsXRef(), pagesAuthors: this.pagesData.loadAuthorsXRef(), authors: this.authorsData.load() }; } /** * Updates existing author in the DB * * @returns {{status: boolean, message: string}} */ updateAuthor() { let sqlQuery = this.db.prepare(`UPDATE authors SET name = @name, username = @slug, password = '', config = @config, additional_data = @additionalData WHERE id = @id`); sqlQuery.run({ name: this.name, slug: slug(this.username), config: this.config, additionalData: JSON.stringify(this.additionalData), id: this.id }); this.checkAndCleanImages(); return { status: true, message: 'author-updated', postsAuthors: this.postsData.loadAuthorsXRef(), authors: this.authorsData.load() }; } /** * Creates author name without leading/ending spaces */ prepareAuthorName() { if(typeof this.name == 'undefined') { this.name = ''; } // Remove leading and ending spaces (trim it) // it will also exclude case when author name contains only // whitespaces this.name = this.name.replace(/^\s+/, '').replace(/\s+$/, ''); } /** * Check if the author name is unique * * @returns {boolean} */ isAuthorNameUnique() { let query = this.db.prepare('SELECT * FROM authors WHERE name LIKE @name AND id != @id'); let queryParams = { name: this.escape(this.name), id: this.id }; let foundedAuthors = query.all(queryParams); if (foundedAuthors.length) { for (const author of foundedAuthors) { if (author.name === this.name) { return false; } } } return true; } /** * Checks if author username (slug) is unique * * @returns {boolean} */ isAuthorUsernameUnique() { let query = this.db.prepare('SELECT username FROM authors WHERE id != @id'); let queryParams = { id: this.id }; let foundedAuthors = query.all(queryParams); if (foundedAuthors.length) { for (const author of foundedAuthors) { if (slug(this.username) === slug(author.username)) { return false; } } } return true; } /** * Removes current author * * @returns {{status: boolean, message: string}} */ delete() { if(this.id === 1) { return { status: false, message: 'cannot-delete-main-author' }; } this.db.exec(`DELETE FROM authors WHERE id = ${parseInt(this.id, 10)}`); this.db.prepare(`UPDATE posts SET authors = '1' WHERE authors LIKE @id`).run({ id: this.id.toString() }); ImageHelper.deleteImagesDirectory(this.siteDir, 'authors', this.id); return { status: true, message: 'author-deleted', posts: this.postsData.load(), postsAuthors: this.postsData.loadAuthorsXRef(), authors: this.authorsData.load() }; } /* * Remove unused images */ checkAndCleanImages (cancelEvent = false) { let authorDir = this.id; if(this.id === 0) { authorDir = 'temp'; } let imagesDir = path.join(this.siteDir, 'input', 'media', 'authors', (authorDir).toString()); let authorDirectoryExists = true; try { fs.statSync(imagesDir).isDirectory(); } catch (err) { authorDirectoryExists = false; } if(!authorDirectoryExists) { return; } let images = fs.readdirSync(imagesDir); this.cleanImages(images, imagesDir, cancelEvent); } /* * Removes images from a given image dir */ cleanImages(images, imagesDir, cancelEvent) { let authorDir = this.id; let featuredImage = ''; let viewConfig = {}; if (this.additionalData && this.additionalData.featuredImage) { featuredImage = path.parse(this.additionalData.featuredImage).base; } if (this.additionalData && this.additionalData.viewConfig) { viewConfig = this.additionalData.viewConfig; } // If author is cancelled - get the previous featured image if (cancelEvent && this.id !== 0) { let additionalDataQuery = `SELECT additional_data FROM authors WHERE id = @id`; let additionalDataResult = this.db.prepare(additionalDataQuery).all({ id: this.id }); if (additionalDataResult) { try { featuredImage = JSON.parse(additionalDataResult[0].additional_data).featuredImage; viewConfig = JSON.parse(additionalDataResult[0].additional_data).viewConfig; } catch (e) { console.log('(!) An issue occurred during parsing author additional data', this.id); } } } if (this.id === 0) { authorDir = 'temp'; } let imagesInViewSettings = []; imagesInViewSettings = Object.keys(viewConfig).filter((fieldName) => { return this.imageConfigFields.indexOf(fieldName) !== -1 && viewConfig[fieldName] !== ''; }).map((fieldName) => { return viewConfig[fieldName]; }); // Iterate through images for (let i in images) { let imagePath = images[i]; let fullPath = path.join(imagesDir, imagePath); // Skip dirs and symlinks if (imagePath === '.' || imagePath === '..' || imagePath === 'responsive') { continue; } // Remove files which does not exist as featured image and authorViewSettings if ( (cancelEvent && authorDir === 'temp') || ( imagesInViewSettings.indexOf(imagePath) === -1 && featuredImage !== imagePath ) ) { try { fs.unlinkSync(fullPath); } catch(e) { console.error(e); } this.removeResponsiveImages(fullPath); } } // Clean unused avatar images let themesHelper = new Themes(this.application, { site: this.site }); let themeConfigPath = path.join(this.application.sitesDir, this.site, 'input', 'config', 'theme.config.json'); if (fs.existsSync(themeConfigPath)) { let themeConfigString = FileHelper.readFileSync(themeConfigPath, 'utf8'); themesHelper.checkAndCleanImages(themeConfigString); } } /* * Remove unused responsive images */ removeResponsiveImages(originalPath) { let themesHelper = new Themes(this.application, { site: this.site }); let currentTheme = themesHelper.currentTheme(); // If there is no selected theme if (currentTheme === 'not selected') { return; } // Load theme config let themeConfig = Utils.loadThemeConfig(path.join(this.siteDir, 'input'), currentTheme); // check if responsive images config exists if(Utils.responsiveImagesConfigExists(themeConfig)) { let dimensions = Utils.responsiveImagesDimensions(themeConfig, 'contentImages'); let featuredDimensions = Utils.responsiveImagesDimensions(themeConfig, 'authorImages'); if (featuredDimensions !== false) { featuredDimensions.forEach(item => { if (dimensions.indexOf(item) === -1) { dimensions.push(item); } }); } let responsiveImagesDir = path.parse(originalPath).dir; responsiveImagesDir = path.join(responsiveImagesDir, 'responsive'); if (typeof dimensions === "boolean") { return; } let forceWebp = !!this.application.sites[this.site]?.advanced?.forceWebp; // Remove responsive images of each size for(let dimensionName of dimensions) { let filename = path.parse(originalPath).name; let extension = path.parse(originalPath).ext; if (forceWebp && ['.png', '.jpg', '.jpeg'].indexOf(extension.toLowerCase()) > -1) { extension = '.webp'; } let responsiveImagePath = path.join(responsiveImagesDir, filename + '-' + dimensionName + extension); if(Utils.fileExists(responsiveImagePath)){ fs.unlinkSync(responsiveImagePath); } } } } } module.exports = Author; ================================================ FILE: app/back-end/authors.js ================================================ /* * Authors instance */ const Model = require('./model.js'); class Authors extends Model { /** * Authors constructor * * @param appInstance * @param authorsData */ constructor(appInstance, authorsData) { super(appInstance, authorsData); } /** * Load authors */ load() { let sqlQuery = `SELECT id, name, username, config, additional_data AS additionalData FROM authors GROUP BY id ORDER BY id ASC`; return this.db.prepare(sqlQuery).all(); } } module.exports = Authors; ================================================ FILE: app/back-end/builddata.json ================================================ { "version": "0.47.5", "build": 17411 } ================================================ FILE: app/back-end/events/_modules.js ================================================ /* * Module which loads all Event classes */ module.exports = { AppEvents: require('./app.js'), ContentEvents: require('./content.js'), CreditsEvents: require('./credits'), ImageUploaderEvents: require('./image-uploader.js'), PageEvents: require('./page.js'), PostEvents: require('./post.js'), SiteEvents: require('./site.js'), TagEvents: require('./tag.js'), TagsEvents: require('./tags.js'), DeployEvents: require('./deploy.js'), SyncEvents: require('./sync.js'), MenuEvents: require('./menu.js'), PreviewEvents: require('./preview.js'), NotificationsEvents: require('./notifications.js'), BackupEvents: require('./backup.js'), AuthorEvents: require('./author.js'), AuthorsEvents: require('./authors.js'), ImportEvents: require('./import.js'), FileManagerEvents: require('./file-manager.js'), PluginEvents: require('./plugin.js'), PluginsApiEvents: require('./plugins-api.js') }; ================================================ FILE: app/back-end/events/app.js ================================================ const fs = require('fs-extra'); const path = require('path'); const FileHelper = require('../helpers/file.js'); const ipcMain = require('electron').ipcMain; const Themes = require('../themes.js'); const Languages = require('../languages.js'); const Plugins = require('../plugins.js'); const AppFiles = require('../helpers/app-files.js'); const AdmZip = require("adm-zip"); /* * Events for the IPC communication regarding app */ class AppEvents { constructor(appInstance) { /* * Close app */ ipcMain.on('app-close', function(event, config) { appInstance.app.quit(); }); /* * Save licence acceptance */ ipcMain.on('app-license-accept', function(event, config) { fs.writeFileSync(appInstance.appConfigPath, JSON.stringify({licenseAccepted: true}, null, 4)); appInstance.appConfig = config; event.sender.send('app-license-accepted', true); }); /* * Save app config */ ipcMain.on('app-config-save', function (event, config) { if (config.sitesLocation === '') { config.sitesLocation = appInstance.dirPaths.sites; } if (config.sitesLocation !== appInstance.appConfig.sitesLocation) { let result = true; if (appInstance.appConfig.sitesLocation) { let appFilesHelper = new AppFiles(appInstance); if (appInstance.db) { try { appInstance.db.close(); } catch (e) { console.log('[SITE LOCATION CHANGE] DB already closed'); } } setTimeout(() => { if (config.changeSitesLocationWithoutCopying) { fs.writeFileSync(appInstance.appConfigPath, JSON.stringify(config, null, 4)); appInstance.appConfig = config; appInstance.sitesDir = config.sitesLocation; } else { result = appFilesHelper.relocateSites( appInstance.appConfig.sitesLocation, config.sitesLocation, event ); if (result) { fs.writeFileSync(appInstance.appConfigPath, JSON.stringify(config, null, 4)); appInstance.appConfig = config; appInstance.sitesDir = config.sitesLocation; } } appInstance.loadSites(); event.sender.send('app-config-saved', { status: true, message: 'success-save', sites: appInstance.sites }); }, 500); return; } } event.sender.send('app-config-saved', { status: true, message: 'success-save' }); fs.writeFileSync(appInstance.appConfigPath, JSON.stringify(config, null, 4)); appInstance.appConfig = config; }); /* * Save app color theme config */ ipcMain.on('app-save-color-theme', function (event, theme) { let appConfig = FileHelper.readFileSync(appInstance.appConfigPath, 'utf8'); try { appConfig = JSON.parse(appConfig); appConfig.appTheme = theme; fs.writeFileSync(appInstance.appConfigPath, JSON.stringify(appConfig, null, 4)); } catch (e) { console.log('(!) App was unable to save the color theme'); } }); /* * Delete theme */ ipcMain.on('app-theme-delete', function(event, config) { let themesLoader = new Themes(appInstance); if(config.directory !== '') { themesLoader.removeTheme(config.directory); appInstance.themes = appInstance.themes.filter(function (theme) { return theme.name !== config.name; }); event.sender.send('app-theme-deleted', { status: true, themes: appInstance.themes }); } }); /* * Delete language */ ipcMain.on('app-language-delete', function(event, config) { let languagesLoader = new Languages(appInstance); if(config.directory !== '') { languagesLoader.removeLanguage(config.directory); appInstance.languages = appInstance.languages.filter(function (language) { return language.name !== config.name; }); event.sender.send('app-language-deleted', { status: true, languages: appInstance.languages }); } }); /* * Delete plugin */ ipcMain.on('app-plugin-delete', function(event, config) { let pluginsLoader = new Plugins(appInstance.appDir, appInstance.sitesDir); if(config.directory !== '') { pluginsLoader.removePlugin(config.directory); appInstance.plugins = appInstance.plugins.filter(function (plugin) { return plugin.name !== config.name; }); event.sender.send('app-plugin-deleted', { status: true, plugins: appInstance.plugins }); } }); /* * Add new theme */ ipcMain.on('app-theme-upload', function(event, config) { let themesLoader = new Themes(appInstance); let newThemeDir = path.parse(config.sourcePath).name; let extension = path.parse(config.sourcePath).ext; let status = ''; if (extension === '.zip' || extension === '') { if (extension === '.zip') { let zipPath = path.join(themesLoader.themesPath, '__TEMP__'); let zip = new AdmZip(config.sourcePath); fs.mkdirSync(zipPath, { recursive: true }); zip.extractAllTo(zipPath, true); let dirs = fs.readdirSync(zipPath).filter(function(file) { if(file.substr(0,1) === '_' || file.substr(0,1) === '.') { return false; } return fs.statSync(path.join(zipPath, file)).isDirectory(); }); if (dirs.length !== 1) { event.sender.send('app-theme-uploaded', { status: 'wrong-format', themes: appInstance.themes }); fs.removeSync(zipPath); return; } newThemeDir = dirs[0]; let directoryPath = path.join(themesLoader.themesPath, newThemeDir); try { fs.statSync(directoryPath); status = 'updated'; fs.removeSync(directoryPath); } catch (e) { status = 'added'; } fs.copySync(path.join(zipPath, newThemeDir), directoryPath); fs.removeSync(zipPath); appInstance.themes = themesLoader.loadThemes(); event.sender.send('app-theme-uploaded', { status: status, themes: appInstance.themes }); return; } else { let directoryPath = path.join(themesLoader.themesPath, newThemeDir); try { fs.statSync(directoryPath); status = 'updated'; fs.removeSync(directoryPath); } catch (e) { status = 'added'; } fs.copySync(config.sourcePath, directoryPath); appInstance.themes = themesLoader.loadThemes(); } } else { status = 'wrong-format'; } event.sender.send('app-theme-uploaded', { status: status, themes: appInstance.themes }); }); /* * Add new language */ ipcMain.on('app-language-upload', function(event, config) { let languagesLoader = new Languages(appInstance); let newLanguageDir = path.parse(config.sourcePath).name; let extension = path.parse(config.sourcePath).ext; let status = ''; if (extension === '.zip' || extension === '') { if (extension === '.zip') { let zipPath = path.join(languagesLoader.languagesPath, '__TEMP__'); let zip = new AdmZip(config.sourcePath); fs.mkdirSync(zipPath, { recursive: true }); zip.extractAllTo(zipPath, true); let dirs = fs.readdirSync(zipPath).filter(function(file) { if(file.substr(0,1) === '_' || file.substr(0,1) === '.') { return false; } return fs.statSync(path.join(zipPath, file)).isDirectory(); }); if (dirs.length !== 1) { event.sender.send('app-language-uploaded', { status: 'wrong-format', languages: appInstance.languages }); fs.removeSync(zipPath); return; } newLanguageDir = dirs[0]; let directoryPath = path.join(languagesLoader.languagesPath, newLanguageDir); try { fs.statSync(directoryPath); status = 'updated'; fs.removeSync(directoryPath); } catch (e) { status = 'added'; } fs.copySync(path.join(zipPath, newLanguageDir), directoryPath); fs.removeSync(zipPath); appInstance.languages = languagesLoader.loadLanguages(); event.sender.send('app-language-uploaded', { status: status, languages: appInstance.languages }); return; } else { let directoryPath = path.join(languagesLoader.languagesPath, newLanguageDir); try { fs.statSync(directoryPath); status = 'updated'; fs.removeSync(directoryPath); } catch (e) { status = 'added'; } fs.copySync(config.sourcePath, directoryPath); appInstance.languages = languagesLoader.loadLanguages(); } } else { status = 'wrong-format'; } event.sender.send('app-language-uploaded', { status: status, languages: appInstance.languages }); }); /* * Add new plugin */ ipcMain.on('app-plugin-upload', function(event, config) { let pluginsLoader = new Plugins(appInstance.appDir, appInstance.sitesDir); let newPluginDir = path.parse(config.sourcePath).name; let extension = path.parse(config.sourcePath).ext; let status = ''; if (extension === '.zip' || extension === '') { if (extension === '.zip') { let zipPath = path.join(pluginsLoader.pluginsPath, '__TEMP__'); fs.mkdirSync(zipPath, { recursive: true }); let zip = new AdmZip(config.sourcePath); zip.extractAllTo(zipPath, true); let dirs = fs.readdirSync(zipPath).filter(function(file) { if(file.substr(0,1) === '_' || file.substr(0,1) === '.') { return false; } return fs.statSync(path.join(zipPath, file)).isDirectory(); }); if (dirs.length !== 1) { event.sender.send('app-plugin-uploaded', { status: 'wrong-format', plugins: appInstance.plugins }); fs.removeSync(zipPath); return; } newPluginDir = dirs[0]; let directoryPath = path.join(pluginsLoader.pluginsPath, newPluginDir); try { fs.statSync(directoryPath); status = 'updated'; fs.removeSync(directoryPath); } catch (e) { status = 'added'; } fs.copySync(path.join(zipPath, newPluginDir), directoryPath); fs.removeSync(zipPath); appInstance.plugins = pluginsLoader.loadPlugins(); event.sender.send('app-plugin-uploaded', { status: status, plugins: appInstance.plugins }); return; } else { let directoryPath = path.join(pluginsLoader.pluginsPath, newPluginDir); try { fs.statSync(directoryPath); status = 'updated'; fs.removeSync(directoryPath); } catch (e) { status = 'added'; } fs.copySync(config.sourcePath, directoryPath); appInstance.plugins = pluginsLoader.loadPlugins(); } } else { status = 'wrong-format'; } event.sender.send('app-plugin-uploaded', { status: status, plugins: appInstance.plugins }); }); /* * Load log files list */ ipcMain.on('app-log-files-load', function(event) { let logPath = appInstance.app.getPath('logs'); let files = fs.readdirSync(logPath).filter(function(file) { return file.substr(-4) === '.txt' || file.substr(-4) === '.log'; }); event.sender.send('app-log-files-loaded', { files: files }); }); /* * Load specific log file */ ipcMain.on('app-log-file-load', function(event, filename) { let logPath = appInstance.app.getPath('logs'); let logFiles = fs.readdirSync(logPath).filter(function(file) { return file.substr(-4) === '.txt' || file.substr(-4) === '.log'; }); if (logFiles.indexOf(filename) === -1) { event.sender.send('app-log-file-loaded', { fileContent: 'File not found!' }); } let filePath = path.join(appInstance.app.getPath('logs'), filename); let fileContent = FileHelper.readFileSync(filePath, 'utf8'); event.sender.send('app-log-file-loaded', { fileContent: fileContent }); }); /* * Set zoom level */ ipcMain.on('app-set-ui-zoom-level', function(event, zoomLevel) { zoomLevel = parseFloat(zoomLevel); if (!zoomLevel || zoomLevel < 0 || zoomLevel > 2.5) { console.log('(!) Invalid zoom level: ', parseFloat(zoomLevel)); return; } let appConfig = FileHelper.readFileSync(appInstance.appConfigPath, 'utf8'); try { appConfig = JSON.parse(appConfig); appConfig.uiZoomLevel = zoomLevel; fs.writeFileSync(appInstance.appConfigPath, JSON.stringify(appConfig, null, 4)); } catch (e) { console.log('(!) App was unable to save the UI zoom level'); } appInstance.mainWindow.webContents.setZoomFactor(zoomLevel); }); /** * Set notifications center state */ ipcMain.on('app-set-notifications-center-state', function(event, state) { let appConfig = fs.readFileSync(appInstance.appConfigPath, 'utf8'); try { appConfig = JSON.parse(appConfig); appConfig.notificationsStatus = state; fs.writeFileSync(appInstance.appConfigPath, JSON.stringify(appConfig, null, 4)); } catch (e) { console.log('(!) App was unable to save the notifications center state'); } }); } } module.exports = AppEvents; ================================================ FILE: app/back-end/events/author.js ================================================ const ipcMain = require('electron').ipcMain; const Author = require('../author.js'); /* * Events for the IPC communication regarding single tags */ class AuthorEvents { constructor(appInstance) { // Save ipcMain.on('app-author-save', function (event, authorData) { let author = new Author(appInstance, authorData); let result = author.save(); event.sender.send('app-author-saved', result); }); // Delete ipcMain.on('app-author-delete', function (event, authorData) { let result = false; for(let i = 0; i < authorData.ids.length; i++) { let author = new Author(appInstance, { site: authorData.site, id: authorData.ids[i] }); result = author.delete(); } event.sender.send('app-author-deleted', result); }); // Cancelled edition ipcMain.on('app-author-cancel', function(event, authorData) { let author = new Author(appInstance, authorData); let result = author.checkAndCleanImages(true); event.sender.send('app-author-cancelled', result); }); } } module.exports = AuthorEvents; ================================================ FILE: app/back-end/events/authors.js ================================================ const ipcMain = require('electron').ipcMain; const Posts = require('../posts.js'); const Authors = require('../authors.js'); /* * Events for the IPC communication regarding authors list */ class AuthorsEvents { constructor(appInstance) { // Load ipcMain.on('app-tags-load', function (event, siteData) { let postsData = new Posts(appInstance, siteData); let authorsData = new Authors(appInstance, siteData); event.sender.send('app-tags-loaded', { authors: authorsData.load(), postsAuthors: postsData.loadAuthorsXRef() }); }); } } module.exports = AuthorsEvents; ================================================ FILE: app/back-end/events/backup.js ================================================ const fs = require('fs-extra'); const path = require('path'); const ipcMain = require('electron').ipcMain; const Backup = require('../modules/backup/backup.js'); class BackupEvents { constructor(appInstance) { let self = this; this.app = appInstance; this.backupsLocation = this.app.appConfig.backupsLocation; if (this.backupsLocation === '') { this.backupsLocation = path.join(this.app.appDir, 'backups'); } ipcMain.on('app-backups-list-load', function (event, siteData) { if(siteData.site) { self.loadBackupsList(siteData.site, event); } else { event.sender.send('app-backups-list-loaded', { status: false }); } }); ipcMain.on('app-backup-create', function(event, siteData) { if(siteData.site) { self.createBackup(siteData.site, siteData.filename, event); } else { event.sender.send('app-backup-created', { status: false }); } }); ipcMain.on('app-backup-remove', function(event, siteData) { if(siteData.site && siteData.backupsNames) { self.removeBackups(siteData.site, siteData.backupsNames, event); } else { event.sender.send('app-backup-removed', { status: false }); } }); ipcMain.on('app-backup-rename', function(event, siteData) { if(siteData.site && siteData.oldBackupName && siteData.newBackupName) { self.renameBackup(siteData.site, siteData.oldBackupName, siteData.newBackupName, event); } else { event.sender.send('app-backup-renamed', { status: false }); } }); ipcMain.on('app-backup-restore', function(event, siteData) { if(siteData.site && siteData.backupName) { self.restoreBackup(siteData.site, siteData.backupName, event); } else { event.sender.send('app-backup-restored', { status: false }); } }); ipcMain.on('app-backup-set-location', (event, newLocation) => { this.backupsLocation = newLocation; if (this.backupsLocation === '') { this.backupsLocation = path.join(this.app.appDir, 'backups'); } }); } loadBackupsList(siteName, event) { let backups = Backup.loadList(siteName, this.backupsLocation); event.sender.send('app-backups-list-loaded', { status: true, backups: backups }); } async removeBackups(siteName, backupsNames, event) { let result = await Backup.remove(siteName, backupsNames, this.backupsLocation); event.sender.send('app-backup-removed', { status: result.status, backups: result.backups }); } renameBackup(siteName, oldBackupName, newBackupName, event) { let result = Backup.rename(siteName, oldBackupName, newBackupName, this.backupsLocation); event.sender.send('app-backup-renamed', { status: result.status, backups: result.backups }); } async createBackup(siteName, filename, event) { let backupsDir = this.backupsLocation; let sourceDir = path.join(this.app.sitesDir, siteName); let backupResult = await Backup.create(siteName, filename, backupsDir, sourceDir); if (backupResult.type === 'app-backup-create-success') { event.sender.send('app-backup-created', { status: true, backups: backupResult.backups }); } else { event.sender.send('app-backup-created', { status: false, error: backupResult.error }); } } async restoreBackup(siteName, backupName, event) { let backupsDir = this.backupsLocation; let destinationDir = this.app.sitesDir; let tempDir = path.join(this.app.appDir, 'temp'); let backupResult = await Backup.restore(siteName, backupName, backupsDir, destinationDir, tempDir, this.app); if (backupResult.type === 'app-backup-restore-success') { event.sender.send('app-backup-restored', { status: true }); } else { event.sender.send('app-backup-restored', { status: false, error: backupResult.error }); } } } module.exports = BackupEvents; ================================================ FILE: app/back-end/events/content.js ================================================ const ipcMain = require('electron').ipcMain; const Model = require('../model.js'); /* * Events for the IPC communication regarding content items */ class ContentEvents { constructor(appInstance) { // Save ipcMain.on('app-content-fields-update', function (event, contentData) { let model = new Model(appInstance, { site: contentData.site }); let result = model.updateField(contentData.table, contentData.itemID, contentData.fieldsToUpdate); event.sender.send('app-content-fields-updated', result); }); } } module.exports = ContentEvents; ================================================ FILE: app/back-end/events/credits.js ================================================ const fs = require('fs-extra'); const path = require('path'); const ipcMain = require('electron').ipcMain; const FileHelper = require('../helpers/file.js'); /* * Events for the IPC communication regarding credits */ class CreditsEvents { constructor(appInstance) { /* * Load license text */ ipcMain.on('app-license-load', function(event, config) { let filePath = path.join(__dirname, '../../', config.url); let licenseText = { translation: 'core.credits.errorLoadingLicenseMsg' } if(fs.existsSync(filePath)) { licenseText = FileHelper.readFileSync(filePath, 'utf-8'); } event.sender.send('app-license-loaded', licenseText); }); } } module.exports = CreditsEvents; ================================================ FILE: app/back-end/events/deploy.js ================================================ const fs = require('fs-extra'); const ipcMain = require('electron').ipcMain; const Deployment = require('../modules/deploy/deployment.js'); const childProcess = require('child_process'); const stripTags = require('striptags'); class DeployEvents { constructor(appInstance) { let self = this; this.app = appInstance; this.deploymentProcess = false; this.rendererProcess = false; ipcMain.on('app-deploy-render', function (event, siteData) { if(siteData.site && siteData.theme) { self.renderSite(siteData.site, event); } else { event.sender.send('app-deploy-rendered', { status: false }); } }); ipcMain.on('app-deploy-render-abort', function(event, siteData) { if(self.rendererProcess) { try { self.rendererProcess.send({ type: 'abort' }); self.rendererProcess = false; } catch(e) { console.log(e); self.rendererProcess = false; } } event.sender.send('app-deploy-aborted', true); }); ipcMain.on('app-deploy-upload', function(event, siteData) { if(siteData.site) { self.deploySite(siteData.site, siteData.password, event.sender); } else { event.sender.send('app-deploy-uploaded', { status: false }); } }); ipcMain.on('app-deploy-abort', function(event, siteData) { if(self.deploymentProcess) { try { self.deploymentProcess.send({ type: 'abort' }); self.deploymentProcess = false; } catch(e) { console.log(e); self.deploymentProcess = false; } } event.sender.send('app-deploy-aborted', true); }); ipcMain.on('app-deploy-continue', function() { if (self.deploymentProcess) { try { self.deploymentProcess.send({ type: 'continue-sync' }); self.deploymentProcess = false; } catch(e) { console.log(e); self.deploymentProcess = false; } } }); ipcMain.on('app-deploy-test', async (event, data) => { try { await this.testConnection(data.deploymentConfig, data.siteName, data.uuid); } catch (err) { console.log('Test connection error:', err); } }); } renderSite(site, event) { this.rendererProcess = childProcess.fork(__dirname + '/../workers/renderer/preview', { stdio: [ null, fs.openSync(this.app.app.getPath('logs') + "/rendering-deployment-process.log", "w"), fs.openSync(this.app.app.getPath('logs') + "/rendering-deployment-errors.log", "w"), 'ipc' ] }); this.rendererProcess.send({ type: 'dependencies', appDir: this.app.appDir, sitesDir: this.app.sitesDir, siteConfig: this.app.sites[site], itemID: false, postData: false, previewMode: false, singlePageMode: false, homepageOnlyMode: false, tagOnlyMode: false, authorOnlyMode: false, previewLocation: this.app.appConfig.previewLocation }); this.rendererProcess.on('message', function(data) { if(data.type === 'app-rendering-results') { if(data.result === true) { event.sender.send('app-deploy-rendered', { status: true }); } else { let errorDesc = { translation: 'core.rendering.renderingProcessCrashedMsg' }; let errorTitle = { translation: 'core.rendering.renderingProcessCrashed' }; if (data.result && data.result[0] && data.result[0].message) { errorTitle = { translation: 'core.rendering.renderingProcessFailed' }; errorDesc = data.result[0].message + "\n\n" + data.result[0].desc; } event.sender.send('app-deploy-render-error', { message: [{ message: errorTitle, desc: stripTags((errorDesc).toString()) }] }); } } else { event.sender.send(data.type, { progress: data.progress, message: stripTags((data.message).toString()) }); } }); } deploySite(site, password, sender) { let self = this; let deploymentConfig = this.app.sites[site]; this.deploymentProcess = childProcess.fork(__dirname + '/../workers/deploy/deployment', { stdio: [ null, fs.openSync(this.app.app.getPath('logs') + "/deployment-process.log", "w"), fs.openSync(this.app.app.getPath('logs') + "/deployment-errors.log", "w"), 'ipc' ] }); if(password !== false) { deploymentConfig.deployment.password = password; } this.deploymentProcess.send({ type: 'dependencies', appDir: this.app.appDir, sitesDir: this.app.sitesDir, siteConfig: deploymentConfig, useFtpAlt: this.app.appConfig.experimentalFeatureAppFtpAlt }); this.deploymentProcess.on('message', function(data) { if (data.type === 'web-contents') { if(data.value) { self.app.mainWindow.webContents.send(data.message, data.value); } else { self.app.mainWindow.webContents.send(data.message); } } if(data.type === 'sender') { sender.send(data.message, data.value); } }); } async testConnection(deploymentConfig, siteName, uuid) { let deployment = new Deployment( this.app.app.getPath('logs'), this.app.sitesDir, deploymentConfig, this.app.appConfig.experimentalFeatureAppFtpAlt ); await deployment.testConnection(this.app, deploymentConfig, siteName, uuid); } } module.exports = DeployEvents; ================================================ FILE: app/back-end/events/file-manager.js ================================================ const fs = require('fs-extra'); const path = require('path'); const ipcMain = require('electron').ipcMain; const isBinaryFileSync = require('isbinaryfile').isBinaryFileSync; /* * Events for the IPC communication regarding file manager */ class FileManagerEvents { /** * Creating an events instance * * @param appInstance */ constructor (appInstance) { let self = this; this.app = appInstance; /* * List files in a specific directory */ ipcMain.on('app-file-manager-list', function(event, config) { self.listFiles(config, event.sender); }); /* * Upload file */ ipcMain.on('app-file-manager-upload', function(event, config) { self.uploadFile(config, event.sender); }); /* * Create file */ ipcMain.on('app-file-manager-create', function(event, config) { self.createFile(config, event.sender); }); /* * Delete files */ ipcMain.on('app-file-manager-delete', function(event, config) { self.deleteFiles(config, event.sender); }); /* * Check filename */ ipcMain.on('app-file-manager-check-name', function(event, config) { self.checkFilename(config, event.sender); }); } /** * Listing files from a specific directory * * @param config * @param sender */ listFiles(config, sender) { let siteName = config.siteName; let dirPath = config.dirPath; let basePath = path.join(this.app.sitesDir, siteName, 'input', dirPath); let files = fs.readdirSync(basePath); let output = []; let iterator = 0; for(let file of files) { if(file === '.DS_Store' || file === 'Thumbs.db') { continue; } let fullPath = path.join(basePath, file); let fileStats = fs.statSync(fullPath); let isDirectory = fileStats.isDirectory(); output.push({ name: file, fullPath: fullPath, icon: this.getIcon(path.parse(file).ext, isDirectory), size: fileStats.size, isBinary: false, isCatalog: isDirectory, createdAt: fileStats.ctime, modifiedAt: fileStats.mtime }); } this.checkIfIsBinaryFile(output, 0, sender); } /** * Checks if files are binary * * @param output * @param iterator * @param sender */ checkIfIsBinaryFile(output, iterator, sender) { if(!output.length || iterator >= output.length) { sender.send('app-file-manager-listed', output); return; } if (output[iterator] && output[iterator].fullPath && fs.lstatSync(output[iterator].fullPath).isFile()) { output[iterator].isBinary = isBinaryFileSync(output[iterator].fullPath); } iterator++; this.checkIfIsBinaryFile(output, iterator, sender); } /** * Returns icon file string according to given extension * * @param extension * @return icon string */ getIcon(extension, isDirectory = false) { if (isDirectory) { return 'catalog'; } extension = extension.replace('.', ''); switch(extension) { case '': case 'txt': case 'rdf': return 'txt'; case 'doc': return 'doc'; case 'docx': return 'docx'; case 'xls': return 'xls'; case 'xlsx': return 'xlsx'; case 'pdf': return 'pdf'; case 'asp': case 'aspx': case 'cfm': case 'cgi': case 'pl': case 'jsp': case 'php': case 'py': case 'rss': case 'xhtml': case 'vue': case 'scss': return 'code'; case 'js': return 'js'; case 'css': return 'css'; case 'html': return 'html'; case 'htm': return 'htm'; case 'xml': return 'xml'; case 'webp': return 'webp'; case 'bmp': return 'img'; case 'tiff': return 'tiff'; case 'avif': return 'avif'; case 'jpg': return 'jpg'; case 'jpeg': return 'jpeg'; case 'png': return 'png'; case 'svg': return 'svg'; case 'gif': return 'gif'; case 'webm': case 'ogg': case 'flv': case 'wmv': case 'm4v': case '3gp': case '3g2': case 'mkv': case 'mpg': case 'mpeg': case 'rm': case 'swf': case 'vob': return 'video'; case 'avi': return 'avi'; case 'mov': return 'mov'; case 'mp4': return 'mp4'; case 'tar': case 'zip': case 'gz': case 'iso': case 'dmg': case 'bz2': case 'lz': case 'ace': case 'apk': case 'jar': return 'zip'; case '7z': return '7z'; case 'rar': return 'rar'; case 'mp3': case '3gp': case 'aac': case 'aax': case 'flac': case 'm4p': case 'ogg': case 'wav': case 'wma': case 'vox': return 'music'; default: return 'unknown'; } } /** * Move file from a given location to specified catalog * * @param config * @param sender */ uploadFile(config, sender) { let siteName = config.siteName; let dirPath = config.dirPath; let fileToMove = config.fileToMove; let fileName = path.parse(fileToMove).base; let destinationPath = path.join(this.app.sitesDir, siteName, 'input', dirPath); let fullPath = path.join(destinationPath, fileName); if(fs.existsSync(fullPath)) { sender.send('app-file-manager-uploaded', false); } fs.copySync(fileToMove, fullPath); sender.send('app-file-manager-uploaded', true); } /** * Create new file * * @param config * @param sender */ createFile(config, sender) { let siteName = config.siteName; let dirPath = config.dirPath; let fileToSave = config.fileToSave; let filePath = path.join(this.app.sitesDir, siteName, 'input', dirPath, fileToSave); if(fs.existsSync(filePath)) { sender.send('app-file-manager-created', false); return; } fs.writeFileSync(filePath, '', {'encoding': 'utf8'}) sender.send('app-file-manager-created', true); } /** * Delete files * * @param config * @param sender */ deleteFiles(config, sender) { let siteName = config.siteName; let dirPath = config.dirPath; let filesToDelete = config.filesToDelete; for(let file of filesToDelete) { let fullPath = path.join(this.app.sitesDir, siteName, 'input', dirPath, file); if(fs.existsSync(fullPath)) { fs.unlinkSync(fullPath); } } sender.send('app-file-manager-deleted', true); } /** * Check if filename exists * * @param config * @param sender */ checkFilename(config, sender) { let siteName = config.siteName; let dirPath = config.dirPath; let filenameToCheck = config.filenameToCheck; let fullPath = path.join(this.app.sitesDir, siteName, 'input', dirPath, filenameToCheck); let result = fs.existsSync(fullPath); sender.send('app-file-manager-checked-name', result); } } module.exports = FileManagerEvents; ================================================ FILE: app/back-end/events/image-uploader.js ================================================ const fs = require('fs'); const path = require('path'); const ipcMain = require('electron').ipcMain; const Image = require('../image.js'); const childProcess = require('child_process'); /* * Events for the IPC communication regarding post images */ class ImageUploaderEvents { constructor(appInstance) { // Upload ipcMain.on('app-image-upload', function (event, imageData) { let imageProcess = childProcess.fork(__dirname + '/../workers/thumbnails/post-images'); imageProcess.send({ type: 'dependencies', appInstance: { appConfig: appInstance.appConfig, appDir: appInstance.appDir, sitesDir: appInstance.sitesDir, db: appInstance.db }, imageData: imageData }); imageProcess.on('message', function(data) { if(data.type === 'image-copied') { imageProcess.send({ type: 'start-regenerating' }); } else if(data.type === 'finished') { event.sender.send('app-image-uploaded', data.result); } }); }); // Remove ipcMain.on('app-image-upload-remove', function (event, filePath, siteName) { let sitePath = path.join(appInstance.sitesDir, siteName); if (filePath.indexOf('media/plugins/') === 0) { filePath = path.join(sitePath, 'input', filePath); } if (filePath.indexOf(sitePath) !== 0) { return; } if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } }); } } module.exports = ImageUploaderEvents; ================================================ FILE: app/back-end/events/import.js ================================================ const fs = require('fs-extra'); const path = require('path'); const ipcMain = require('electron').ipcMain; const Import = require('../modules/import/import.js'); const childProcess = require('child_process'); /* * Events for the IPC communication regarding imports */ class ImportEvents { /** * Creating an events instance * * @param appInstance */ constructor(appInstance) { let self = this; this.app = appInstance; /* * Import WXR file */ ipcMain.on('app-wxr-check', function(event, config) { self.checkFile(config.siteName, config.filePath, event.sender); }); ipcMain.on('app-wxr-import', function(event, config) { self.importFile(appInstance, config, event.sender); }); } /** * Checking the WXR file * * @param siteName * @param filePath */ checkFile(siteName, filePath, sender) { let importProcess = childProcess.fork(__dirname + '/../workers/import/check', { stdio: [ null, fs.openSync(this.app.app.getPath('logs') + "/import-check-process.log", "w"), fs.openSync(this.app.app.getPath('logs') + "/import-check-errors.log", "w"), 'ipc' ] }); importProcess.send({ type: 'dependencies', siteName: siteName, filePath: filePath }); importProcess.on('message', function(data) { sender.send('app-wxr-checked', data); }); } /** * Imports data from the WXR file * * @param appInstance * @param config */ importFile(appInstance, config, sender) { let importProcess = childProcess.fork(__dirname + '/../workers/import/import', { stdio: [ null, fs.openSync(this.app.app.getPath('logs') + "/import-process.log", "w"), fs.openSync(this.app.app.getPath('logs') + "/import-errors.log", "w"), 'ipc' ] }); importProcess.send({ type: 'dependencies', appInstance: { appDir: appInstance.appDir, sitesDir: appInstance.sitesDir, sites: appInstance.sites }, siteName: config.siteName, filePath: config.filePath, importAuthors: config.importAuthors, usedTaxonomy: config.usedTaxonomy, autop: config.autop, postTypes: config.postTypes }); importProcess.on('message', function(data) { if(data.type === 'result') { sender.send('app-wxr-imported', data); } else { sender.send('app-wxr-import-progress', data); } }); } } module.exports = ImportEvents; ================================================ FILE: app/back-end/events/menu.js ================================================ const fs = require('fs-extra'); const path = require('path'); const ipcMain = require('electron').ipcMain; /* * Events for the IPC communication regarding menu events */ class MenuEvents { constructor(appInstance) { let self = this; this.app = appInstance; /* * Save information about menu */ ipcMain.on('app-menu-update', function (event, data) { self.saveNewMenuStructure(data.menuStructure, data.siteName); event.sender.send('app-menu-updated', true); }); } saveNewMenuStructure(menuStructure, siteName) { let menuFile = path.join(this.app.sitesDir, siteName, 'input', 'config', 'menu.config.json'); fs.writeFileSync(menuFile, JSON.stringify(menuStructure, null, 4), { encoding: 'utf8' }); } } module.exports = MenuEvents; ================================================ FILE: app/back-end/events/notifications.js ================================================ const ipcMain = require('electron').ipcMain; const path = require('path'); const UpdatesHelper = require('../helpers/updates.helper.js'); /* * Events for the IPC communication regarding notifications */ class NotificationsEvents { constructor(appInstance) { // Save ipcMain.on('app-notifications-retrieve', function(event, downloadNotifications) { let platform; if (process.platform === 'darwin') { platform = process.arch === 'arm64' ? 'mac-arm64' : 'mac-x86'; } else if (process.platform === 'win32') { platform = 'win'; } else { platform = 'linux'; } let updatesHelper = new UpdatesHelper({ event: event, filePath: path.join(appInstance.app.getPath('logs'), 'updates.json'), url: 'https://notifications.getpublii.com/updates-' + platform + '.json', forceDownload: downloadNotifications }); updatesHelper.retrieve(); }); // Get notifications file ipcMain.handle('app-get-notifications-file', async (event, fileName) => { let filePath = path.join(appInstance.app.getPath('logs'), fileName); try { let data = await appInstance.app.readFile(filePath, 'utf8'); return JSON.parse(data); } catch (error) { console.error(`Error reading notifications file: ${error.message}`); return false; } }); } } module.exports = NotificationsEvents; ================================================ FILE: app/back-end/events/page.js ================================================ const fs = require('fs'); const path = require('path'); const FileHelper = require('../helpers/file.js'); const ipcMain = require('electron').ipcMain; const Page = require('../page.js'); /* * Events for the IPC communication regarding pages */ class PageEvents { constructor(appInstance) { this.app = appInstance; // Load ipcMain.on('app-page-load', function (event, pageData) { let page = new Page(appInstance, pageData); let result = page.load(); event.sender.send('app-page-loaded', result); }); // Save ipcMain.on('app-page-save', function (event, pageData) { let page = new Page(appInstance, pageData); let result = page.save(); event.sender.send('app-page-saved', result); }); // Delete ipcMain.on('app-page-delete', function (event, pageData) { let result = false; for(let i = 0; i < pageData.ids.length; i++) { let page = new Page(appInstance, { site: pageData.site, id: pageData.ids[i] }); result = page.delete(); } event.sender.send('app-page-deleted', result); }); // Delete ipcMain.on('app-page-duplicate', function (event, pageData) { let result = false; for(let i = 0; i < pageData.ids.length; i++) { let page = new Page(appInstance, { site: pageData.site, id: pageData.ids[i] }); result = page.duplicate(); } event.sender.send('app-page-duplicated', result); }); // Status change ipcMain.on('app-page-status-change', function (event, pageData) { let result = false; for(let i = 0; i < pageData.ids.length; i++) { let page = new Page(appInstance, { site: pageData.site, id: pageData.ids[i] }); result = page.changeStatus(pageData.status, pageData.inverse); } event.sender.send('app-page-status-changed', result); }); // Cancelled edition ipcMain.on('app-page-cancel', function(event, pageData) { let page = new Page(appInstance, pageData); let result = page.checkAndCleanImages(true); event.sender.send('app-page-cancelled', result); }); // Load pages hierarchy ipcMain.on('app-pages-hierarchy-load', (event, siteName) => { let pagesFile = path.join(this.app.sitesDir, siteName, 'input', 'config', 'pages.config.json'); if (fs.existsSync(pagesFile)) { let pagesHierarchy = JSON.parse(FileHelper.readFileSync(pagesFile, { encoding: 'utf8' })); pagesHierarchy = this.removeDuplicatedDataFromHierarchy(pagesHierarchy); event.sender.send('app-pages-hierarchy-loaded', pagesHierarchy); } else { event.sender.send('app-pages-hierarchy-loaded', null); } }); // Save pages hierarchy ipcMain.on('app-pages-hierarchy-save', (event, pagesData) => { let pagesFile = path.join(this.app.sitesDir, pagesData.siteName, 'input', 'config', 'pages.config.json'); pagesData.hierarchy = this.removeNullDataFromHierarchy(pagesData.hierarchy); pagesData.hierarchy = this.removeDuplicatedDataFromHierarchy(pagesData.hierarchy); fs.writeFileSync(pagesFile, JSON.stringify(pagesData.hierarchy, null, 4), { encoding: 'utf8' }); }); // Update pages hierarchy during post conversion ipcMain.on('app-pages-hierarchy-update', (event, conversionData) => { let pagesFile = path.join(this.app.sitesDir, conversionData.siteName, 'input', 'config', 'pages.config.json'); let pagesHierarchy = JSON.parse(FileHelper.readFileSync(pagesFile, { encoding: 'utf8' })); for (let i = 0; i < conversionData.postIDs.length; i++) { pagesHierarchy.push({ id: conversionData.postIDs[i], subpages: [] }); } pagesHierarchy = this.removeNullDataFromHierarchy(pagesHierarchy); pagesHierarchy = this.removeDuplicatedDataFromHierarchy(pagesHierarchy); fs.writeFileSync(pagesFile, JSON.stringify(pagesHierarchy, null, 4), { encoding: 'utf8' }); }); } removeNullDataFromHierarchy (data) { return data .filter(item => item !== null) .map(item => ({ ...item, subpages: item.subpages ? this.removeNullDataFromHierarchy(item.subpages) : [] })); } removeDuplicatedDataFromHierarchy (data) { let existingIds = new Set(); return data.filter(item => { if (existingIds.has(item.id)) { return false; } existingIds.add(item.id); return true; }); } } module.exports = PageEvents; ================================================ FILE: app/back-end/events/plugin.js ================================================ const ipcMain = require('electron').ipcMain; const Plugins = require('../plugins.js'); /* * Events for the IPC communication regarding plugins */ class PluginEvents { constructor(appInstance) { // Get plugins status ipcMain.on('app-site-get-plugins-state', function (event, data) { let pluginsInstance = new Plugins(appInstance.appDir, appInstance.sitesDir); let siteName = data.siteName.replace(/[\/\\]/gmi, ''); let pluginsStatus = pluginsInstance.getSiteSpecificPluginsState(siteName); event.sender.send('app-site-plugins-state-loaded', pluginsStatus); }); // Activate ipcMain.on('app-site-plugin-activate', function (event, data) { let pluginsInstance = new Plugins(appInstance.appDir, appInstance.sitesDir); let siteName = data.siteName.replace(/[\/\\]/gmi, ''); let pluginName = data.pluginName.replace(/[\/\\]/gmi, ''); let result = pluginsInstance.activatePlugin(siteName, pluginName); event.sender.send('app-site-plugin-activated', result); }); // Deactivate ipcMain.on('app-site-plugin-deactivate', function (event, data) { let pluginsInstance = new Plugins(appInstance.appDir, appInstance.sitesDir); let siteName = data.siteName.replace(/[\/\\]/gmi, ''); let pluginName = data.pluginName.replace(/[\/\\]/gmi, ''); let result = pluginsInstance.deactivatePlugin(siteName, pluginName); event.sender.send('app-site-plugin-deactivated', result); }); // Get plugin info and config ipcMain.on('app-site-get-plugin-config', function (event, data) { let pluginsInstance = new Plugins(appInstance.appDir, appInstance.sitesDir); let siteName = data.siteName.replace(/[\/\\]/gmi, ''); let pluginName = data.pluginName.replace(/[\/\\]/gmi, ''); let result = pluginsInstance.getPluginConfig(siteName, pluginName); event.sender.send('app-site-get-plugin-config-retrieved', result); }); // Save plugin config ipcMain.on('app-site-save-plugin-config', function (event, data) { let pluginsInstance = new Plugins(appInstance.appDir, appInstance.sitesDir); let siteName = data.siteName.replace(/[\/\\]/gmi, ''); let pluginName = data.pluginName.replace(/[\/\\]/gmi, ''); let result = pluginsInstance.savePluginConfig(siteName, pluginName, data.newConfig); event.sender.send('app-site-plugin-config-saved', result); }); } } module.exports = PluginEvents; ================================================ FILE: app/back-end/events/plugins-api.js ================================================ const ipcMain = require('electron').ipcMain; const fs = require('fs'); const path = require('path'); const FileHelper = require('../helpers/file.js'); /* * Events for the IPC communication regarding plugins */ class PluginsApiEvents { constructor (appInstance) { // Read file in site ipcMain.handle('app-plugins-api:read-config-file', function (event, data) { let fileName = data.fileName.replace(/a-zA-Z0-9\-\_\.\*\@\+/gmi, ''); let siteName = data.siteName.replace(/[\/\\]/gmi, ''); let pluginName = data.pluginName.replace(/[\/\\]/gmi, ''); let filePath = path.join(appInstance.sitesDir, siteName, 'input', 'config', 'plugins', pluginName, fileName); if (!fs.existsSync(filePath)) { return false; } let fileContent = FileHelper.readFileSync(filePath); fileContent = fileContent.toString(); return fileContent; }); // Read file in the languages ipcMain.handle('app-plugins-api:read-language-file', function (event, data) { let fileName = data.fileName.replace(/a-zA-Z0-9\-\_\.\*\@\+/gmi, ''); let siteName = data.siteName.replace(/[\/\\]/gmi, ''); let filePath = path.join(appInstance.sitesDir, siteName, 'input', 'languages', fileName); if (!fs.existsSync(filePath)) { return false; } let fileContent = FileHelper.readFileSync(filePath); fileContent = fileContent.toString(); return fileContent; }); // Read file in the themes ipcMain.handle('app-plugins-api:read-theme-file', function (event, data) { let fileName = data.fileName.replace(/a-zA-Z0-9\-\_\.\*\@\+/gmi, ''); let themeName = data.themeName.replace(/a-zA-Z0-9\-\_\.\*\@\+/gmi, ''); let siteName = data.siteName.replace(/[\/\\]/gmi, ''); let filePath = path.join(appInstance.sitesDir, siteName, 'input', 'themes', themeName, fileName); if (!fs.existsSync(filePath)) { return false; } let fileContent = FileHelper.readFileSync(filePath); fileContent = fileContent.toString(); return fileContent; }); // Save file in site ipcMain.handle('app-plugins-api:save-config-file', function (event, data) { let fileName = data.fileName.replace(/a-zA-Z0-9\-\_\.\*\@\+/gmi, ''); let siteName = data.siteName.replace(/[\/\\]/gmi, ''); let pluginName = data.pluginName.replace(/[\/\\]/gmi, ''); let filePath = path.join(appInstance.sitesDir, siteName, 'input', 'config', 'plugins', pluginName, fileName); if (!fs.existsSync(path.join(appInstance.sitesDir, siteName, 'input', 'config', 'plugins', pluginName))) { fs.mkdirSync(path.join(appInstance.sitesDir, siteName, 'input', 'config', 'plugins', pluginName), { recursive: true }); } try { fs.writeFileSync(filePath, data.fileContent); return { status: 'FILE_SAVED' }; } catch (e) { return { status: 'FILE_NOT_SAVED' }; } }); // Save file in languages ipcMain.handle('app-plugins-api:save-language-file', function (event, data) { let fileName = data.fileName.replace(/a-zA-Z0-9\-\_\.\*\@\+/gmi, ''); let siteName = data.siteName.replace(/[\/\\]/gmi, ''); let dirPath = path.join(appInstance.sitesDir, siteName, 'input', 'languages'); let filePath = path.join(appInstance.sitesDir, siteName, 'input', 'languages', fileName); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } try { fs.writeFileSync(filePath, data.fileContent); return { status: 'FILE_SAVED' }; } catch (e) { return { status: 'FILE_NOT_SAVED' }; } }); // Delete file in site ipcMain.handle('app-plugins-api:delete-config-file', function (event, data) { let fileName = data.fileName.replace(/a-zA-Z0-9\-\_\.\*\@\+/gmi, ''); let siteName = data.siteName.replace(/[\/\\]/gmi, ''); let pluginName = data.pluginName.replace(/[\/\\]/gmi, ''); let filePath = path.join(appInstance.sitesDir, siteName, 'input', 'config', 'plugins', pluginName, fileName); if (!fs.existsSync(filePath)) { return { status: 'FILE_TO_REMOVE_NOT_EXISTS' }; } try { fs.unlinkSync(filePath); return { status: 'FILE_REMOVED' }; } catch (e) { return { status: 'FILE_NOT_REMOVED' }; } }); // Delete file in site ipcMain.handle('app-plugins-api:delete-language-file', function (event, data) { let fileName = data.fileName.replace(/a-zA-Z0-9\-\_\.\*\@\+/gmi, ''); let siteName = data.siteName.replace(/[\/\\]/gmi, ''); let filePath = path.join(appInstance.sitesDir, siteName, 'input', 'languages', fileName); if (!fs.existsSync(filePath)) { return { status: 'FILE_TO_REMOVE_NOT_EXISTS' }; } try { fs.unlinkSync(filePath); return { status: 'FILE_REMOVED' }; } catch (e) { return { status: 'FILE_NOT_REMOVED' }; } }); } } module.exports = PluginsApiEvents; ================================================ FILE: app/back-end/events/post.js ================================================ const ipcMain = require('electron').ipcMain; const Post = require('../post.js'); /* * Events for the IPC communication regarding single tags */ class PostEvents { constructor(appInstance) { // Load ipcMain.on('app-post-load', function (event, postData) { let post = new Post(appInstance, postData); let result = post.load(); event.sender.send('app-post-loaded', result); }); // Save ipcMain.on('app-post-save', function (event, postData) { let post = new Post(appInstance, postData); let result = post.save(); event.sender.send('app-post-saved', result); }); // Delete ipcMain.on('app-post-delete', function (event, postData) { let result = false; for(let i = 0; i < postData.ids.length; i++) { let post = new Post(appInstance, { site: postData.site, id: postData.ids[i] }); result = post.delete(); } event.sender.send('app-post-deleted', result); }); // Delete ipcMain.on('app-post-duplicate', function (event, postData) { let result = false; for(let i = 0; i < postData.ids.length; i++) { let post = new Post(appInstance, { site: postData.site, id: postData.ids[i] }); result = post.duplicate(); } event.sender.send('app-post-duplicated', result); }); // Status change ipcMain.on('app-post-status-change', function (event, postData) { let result = false; for(let i = 0; i < postData.ids.length; i++) { let post = new Post(appInstance, { site: postData.site, id: postData.ids[i] }); result = post.changeStatus(postData.status, postData.inverse); } event.sender.send('app-post-status-changed', result); }); // Cancelled edition ipcMain.on('app-post-cancel', function(event, postData) { let post = new Post(appInstance, postData); let result = post.checkAndCleanImages(true); event.sender.send('app-post-cancelled', result); }); } } module.exports = PostEvents; ================================================ FILE: app/back-end/events/preview.js ================================================ const fs = require('fs'); const path = require('path'); const electron = require('electron'); const shell = electron.shell; const ipcMain = electron.ipcMain; const childProcess = require('child_process'); const UtilsHelper = require('../helpers/utils.js'); const stripTags = require('striptags'); class PreviewEvents { /** * Creates preview events * * @param appInstance */ constructor(appInstance) { let self = this; this.app = appInstance; ipcMain.on('app-preview-render', function (event, siteData) { if (siteData.site && siteData.theme) { let itemID = false; let mode = false; let postData = false; let showPreview = true; if (siteData.itemID !== false && typeof siteData.itemID !== 'undefined') { itemID = siteData.itemID; } if (siteData.mode !== false && typeof siteData.mode !== 'undefined') { mode = siteData.mode; } if (siteData.postData) { postData = siteData.postData; } if (typeof siteData.showPreview !== 'undefined') { showPreview = siteData.showPreview; } self.renderSite(siteData.site, itemID, postData, mode, event, showPreview); } else { event.sender.send('app-preview-rendered', { status: false }); } }); } /** * Renders website * * @param site * @param pageToRender * @param postData * @param event */ renderSite(site, itemID, postData, mode, event, showPreview) { let self = this; let previewMode = true; let resultsRetrieved = false; let rendererProcess = childProcess.fork(__dirname + '/../workers/renderer/preview', { stdio: [ null, fs.openSync(this.app.app.getPath('logs') + "/rendering-process.log", "w"), fs.openSync(this.app.app.getPath('logs') + "/rendering-errors.log", "w"), 'ipc' ] }); rendererProcess.on('disconnect', function(data) { setTimeout(function() { if(!resultsRetrieved) { let errorDesc = { translation: 'core.rendering.renderingProcessCrashedMsg' }; let errorTitle = { translation: 'core.rendering.renderingProcessCrashed' }; if (data && data.result && data.result[0] && data.result[0].message) { errorTitle = { translation: 'core.rendering.renderingProcessFailed' }; errorDesc = stripTags((data.result[0].message + "\n\n" + data.result[0].desc).toString()); } event.sender.send('app-preview-render-error', { message: [{ message: errorTitle, desc: errorDesc }] }); } }, 1000); }); rendererProcess.send({ type: 'dependencies', appDir: this.app.appDir, sitesDir: this.app.sitesDir, siteConfig: this.app.sites[site], itemID: itemID, postData: postData, previewMode: previewMode, mode: mode, previewLocation: this.app.appConfig.previewLocation }); rendererProcess.on('message', function(data) { resultsRetrieved = true; if(data.type === 'app-rendering-results') { if(data.result === true) { event.sender.send('app-preview-rendered', { status: true }); if (showPreview) { self.showPreview(site, mode); } } else { let errorDesc = 'core.rendering.renderingProcessCrashedMsg'; let errorTitle = 'core.rendering.renderingProcessCrashed'; if (data.result && data.result[0] && data.result[0].message) { errorTitle = { translation: 'core.rendering.renderingProcessFailed' }; errorDesc = stripTags((data.result[0].message + "\n\n" + data.result[0].desc).toString()); } event.sender.send('app-preview-render-error', { message: [{ message: errorTitle, desc: errorDesc }] }); } } else { event.sender.send(data.type, { progress: data.progress, message: stripTags((data.message).toString()) }); } }); } /** * Displays preview * * @param siteData */ showPreview (siteName, mode) { let basePath = path.join(this.app.sitesDir, siteName, 'preview'); let previewLocation = ''; if(this.app.appConfig.previewLocation) { previewLocation = this.app.appConfig.previewLocation.trim(); } let url = ''; if(previewLocation !== '' && UtilsHelper.dirExists(previewLocation)) { basePath = previewLocation; } url = path.join(basePath, 'index.html'); if (mode === 'tag' || mode === 'post' || mode === 'page' || mode === 'author') { url = path.join(basePath, 'preview.html'); } setTimeout(function() { shell.openExternal('file:///' + url); }, 1000); } } module.exports = PreviewEvents; ================================================ FILE: app/back-end/events/site.js ================================================ const fs = require('fs-extra'); const os = require('os'); const path = require('path'); const FileHelper = require('../helpers/file.js'); const slug = require('./../helpers/slug'); const passwordSafeStorage = require('keytar'); const ipcMain = require('electron').ipcMain; const Site = require('../site.js'); const Themes = require('../themes.js'); const Database = os.platform() === 'linux' ? require('node-sqlite3-wasm').Database : require('better-sqlite3'); const DBUtils = require('../helpers/db.utils.js'); const UtilsHelper = require('../helpers/utils.js'); const normalizePath = require('normalize-path'); const URLHelper = require('../modules/render-html/helpers/url.js'); /* * Events for the IPC communication regarding single sites */ class SiteEvents { constructor(appInstance) { let self = this; this.regenerateProcess = false; /* * Reload site config and data */ ipcMain.on('app-site-reload', (event, config) => { let result = appInstance.reloadSite(config.siteName); let language = this.getSiteLanguage(appInstance, config.siteName); this.setSpellcheckerLanguage (appInstance, language); event.sender.send('app-site-reloaded', result); }); /* * Save site config */ ipcMain.on('app-site-config-save', async function (event, config) { let siteName = ''; let siteNames = Object.keys(appInstance.sites); let thumbnailsRegenerateRequired = false; if (siteNames.indexOf(config.site) !== -1) { siteName = config.site; } else { event.sender.send('app-site-config-saved', { status: false, message: 'site-not-exists' }); } if (config.source === 'server') { self.removeGitConfigDirectory(appInstance, config.site); } // Prepare settings config.settings.name = slug(config.settings.name); config.settings.advanced.urls.postsPrefix = slug(config.settings.advanced.urls.postsPrefix); config.settings.advanced.urls.tagsPrefix = slug(config.settings.advanced.urls.tagsPrefix); config.settings.advanced.urls.authorsPrefix = slug(config.settings.advanced.urls.authorsPrefix); config.settings.advanced.urls.pageName = slug(config.settings.advanced.urls.pageName); config.settings.advanced.urls.errorPage = slug(config.settings.advanced.urls.errorPage, true); config.settings.advanced.urls.searchPage = slug(config.settings.advanced.urls.searchPage, true); // If user changed the site name if ( config.settings.name !== '' && siteName !== '' && slug(config.settings.name) !== slug(config.site) ) { // Check if new site name is unique if ( !fs.existsSync(path.join(appInstance.sitesDir, config.settings.name)) && slug(config.settings.displayName) === config.settings.name ) { if (appInstance.db) { try { appInstance.db.close(); } catch (e) { console.log('[SITE NAME CHANGE] DB already closed'); } } // If yes - rename the dir delete appInstance.sites[siteName]; siteName = config.settings.name; fs.renameSync( path.join(appInstance.sitesDir, config.site), path.join(appInstance.sitesDir, config.settings.name) ); let dbPath = path.join(appInstance.sitesDir, config.settings.name, 'input', 'db.sqlite'); appInstance.db = new DBUtils(new Database(dbPath)); // Rename also the backups directory let backupsDir = appInstance.appConfig.backupsLocation; if(backupsDir) { let backupsLocation = path.join(backupsDir, config.site); let newBackupsLocation = path.join(backupsDir, config.settings.name); if (UtilsHelper.dirExists(backupsLocation)) { fs.renameSync( backupsLocation, newBackupsLocation ); } } } else { // If no - return error event.sender.send('app-site-config-saved', { status: false, message: 'duplicated-name' }); return; } } // Check for empty names if ( siteName === '' || config.settings.name === '' ) { event.sender.send('app-site-config-saved', { status: false, message: 'empty-name' }); return; } let configFile = path.join(appInstance.sitesDir, siteName, 'input', 'config', 'site.config.json'); let oldConfig = FileHelper.readFileSync(configFile, 'utf8'); let themesPath = path.join(appInstance.sitesDir, siteName, 'input', 'themes'); let newThemeConfig = {}; oldConfig = JSON.parse(oldConfig); if (config.settings.theme === '' && oldConfig.theme) { config.settings.theme = oldConfig.theme; } else { let themes = new Themes(appInstance, { site: siteName }); if(self.prepareThemeName(config.settings.theme) !== oldConfig.theme) { thumbnailsRegenerateRequired = true; } config.settings.theme = themes.changeTheme(config.settings.theme, oldConfig.theme); let themeConfigPath = path.join(appInstance.sitesDir, siteName, 'input', 'config', 'theme.config.json'); let themePath = path.join(themesPath, config.settings.theme); newThemeConfig = Themes.loadThemeConfig(themeConfigPath, themePath); } if ( oldConfig.advanced && ( (oldConfig.advanced.responsiveImages !== config.settings.advanced.responsiveImages) || (oldConfig.advanced.imagesQuality !== config.settings.advanced.imagesQuality) || (oldConfig.advanced.alphaQuality !== config.settings.advanced.alphaQuality) || (oldConfig.advanced.forceWebp !== config.settings.advanced.forceWebp) || (oldConfig.advanced.webpLossless !== config.settings.advanced.webpLossless) ) ) { thumbnailsRegenerateRequired = true; } if( config.settings.advanced && config.settings.advanced.openGraphImage && config.settings.advanced.openGraphImage !== '' ) { let filename = path.parse(config.settings.advanced.openGraphImage); config.settings.advanced.openGraphImage = filename.base; } let passwordData = false; let passwordGitData = false; let passphraseData = false; let s3IdData = false; let s3KeyData = false; let ghTokenData = false; let glTokenData = false; let netlifyIdData = false; let netlifyTokenData = false; let siteID = slug(config.settings.name); if (config.settings.uuid) { siteID = config.settings.uuid; } // Save the password in the keychain if (passwordSafeStorage) { try { if ( config.settings.deployment.password !== '' && config.settings.deployment.password !== 'publii ' + siteID ) { passwordData = await self.loadPassword( config.settings, 'publii', config.settings.deployment.password ); config.settings.deployment.password = passwordData.toSave; } if ( config.settings.deployment.git.password !== '' && config.settings.deployment.git.password !== 'publii-git-password ' + siteID ) { passwordGitData = await self.loadPassword( config.settings, 'publii-git-password', config.settings.deployment.git.password ); config.settings.deployment.git.password = passwordGitData.toSave; } if ( config.settings.deployment.passphrase !== '' && config.settings.deployment.passphrase !== 'publii-passphrase ' + siteID ) { passphraseData = await self.loadPassword( config.settings, 'publii-passphrase', config.settings.deployment.passphrase ); config.settings.deployment.passphrase = passphraseData.toSave; } if ( config.settings.deployment.s3.id !== '' && config.settings.deployment.s3.key !== '' && config.settings.deployment.s3.id !== 'publii-s3-id ' + siteID ) { s3IdData = await self.loadPassword( config.settings, 'publii-s3-id', config.settings.deployment.s3.id ); s3KeyData = await self.loadPassword( config.settings, 'publii-s3-key', config.settings.deployment.s3.key ); config.settings.deployment.s3.id = s3IdData.toSave; config.settings.deployment.s3.key = s3KeyData.toSave; } if ( config.settings.deployment.github.token !== '' && config.settings.deployment.github.token !== 'publii-gh-token ' + siteID ) { ghTokenData = await self.loadPassword( config.settings, 'publii-gh-token', config.settings.deployment.github.token ); config.settings.deployment.github.token = ghTokenData.toSave; } if ( config.settings.deployment.gitlab.token !== '' && config.settings.deployment.gitlab.token !== 'publii-gl-token ' + siteID ) { glTokenData = await self.loadPassword( config.settings, 'publii-gl-token', config.settings.deployment.gitlab.token ); config.settings.deployment.gitlab.token = glTokenData.toSave; } if ( config.settings.deployment.netlify.id !== '' && config.settings.deployment.netlify.token !== '' && config.settings.deployment.netlify.token !== 'publii-netlify-token ' + siteID ) { netlifyIdData = await self.loadPassword( config.settings, 'publii-netlify-id', config.settings.deployment.netlify.id ); netlifyTokenData = await self.loadPassword( config.settings, 'publii-netlify-token', config.settings.deployment.netlify.token ); config.settings.deployment.netlify.id = netlifyIdData.toSave; config.settings.deployment.netlify.token = netlifyTokenData.toSave; } } catch (error) { event.sender.send('app-site-config-saved', { status: false, message: 'no-keyring' }); return; } } // Save config fs.writeFileSync(configFile, JSON.stringify(config.settings, null, 4)); if(passwordData && passwordData.newPassword) { config.settings.deployment.password = passwordData.newPassword; } if(passwordGitData && passwordGitData.newPassword) { config.settings.deployment.git.password = passwordGitData.newPassword; } if(passphraseData && passphraseData.newPassword) { config.settings.deployment.passphrase = passphraseData.newPassword; } if(s3IdData && s3IdData.newPassword) { config.settings.deployment.s3.id = s3IdData.newPassword; } if(s3KeyData && s3KeyData.newPassword) { config.settings.deployment.s3.key = s3KeyData.newPassword; } if(ghTokenData && ghTokenData.newPassword) { config.settings.deployment.github.token = ghTokenData.newPassword; } if(netlifyIdData && netlifyIdData.newPassword) { config.settings.deployment.netlify.id = netlifyIdData.newPassword; } if(netlifyTokenData && netlifyTokenData.newPassword) { config.settings.deployment.netlify.token = netlifyTokenData.newPassword; } appInstance.sites[config.settings.name] = config.settings; let themesHelper = new Themes(appInstance, { site: siteName }); let themeConfigPath = path.join(appInstance.sitesDir, siteName, 'input', 'config', 'theme.config.json'); if (fs.existsSync(themeConfigPath)) { let themeConfigString = FileHelper.readFileSync(themeConfigPath, 'utf8'); themesHelper.checkAndCleanImages(themeConfigString); } // Send success message event.sender.send('app-site-config-saved', { status: true, siteName: siteName, message: 'success-save', themeName: config.settings.theme, newThemeConfig: newThemeConfig, thumbnailsRegenerateRequired: thumbnailsRegenerateRequired }); }); /* * Switch website */ ipcMain.on('app-site-switch', (event, config) => { let result = appInstance.switchSite(config.site); let language = this.getSiteLanguage(appInstance, config.site); this.setSpellcheckerLanguage (appInstance, language); event.sender.send('app-site-switched', result); }); /* * Refresh website data */ ipcMain.on('app-site-refresh', function (event, config) { let result = appInstance.switchSite(config.site); event.sender.send('app-site-refreshed', result); }); /* * Save site theme config */ ipcMain.on('app-site-theme-config-save', function (event, data) { let siteData = { site: data.site }; let newConfig = data.config; let themeName = data.theme; let themePath = path.join(appInstance.sitesDir, data.site, 'input', 'themes', themeName); let themeConfigPath = path.join(appInstance.sitesDir, data.site, 'input', 'config', 'theme.config.json'); let themesHelper = new Themes(appInstance, siteData); themesHelper.updateThemeConfig(newConfig); let themeConfig = Themes.loadThemeConfig(themeConfigPath, themePath); event.sender.send('app-site-theme-config-saved', { status: true, newConfig: { config: themeConfig.config, customConfig: themeConfig.customConfig, pageConfig: themeConfig.pageConfig, postConfig: themeConfig.postConfig, tagConfig: themeConfig.tagConfig, authorConfig: themeConfig.authorConfig, defaultTemplates: themeConfig.defaultTemplates } }); }); /* * Create new website */ ipcMain.on('app-site-create', function (event, config, authorName) { config.name = slug(config.name); if (config.name.trim() === '') { event.sender.send('app-site-creation-error', { name: config.name.trim() === '', author: slug(authorName).trim() === '' }); return; } let site = new Site(appInstance, config); let result = site.create(authorName); if (result === 'db-error') { event.sender.send('app-site-creation-db-error'); return; } if (result === 'duplicate') { event.sender.send('app-site-creation-duplicate'); return; } config.uuid = site.uuid; config.theme = 'simple'; appInstance.sites[config.name] = config; // Load newly created db let siteDir = path.join(appInstance.sitesDir, config.name); let dbPath = path.join(siteDir, 'input', 'db.sqlite'); if (appInstance.db) { try { appInstance.db.close(); } catch (e) { console.log('[SITE CREATION] DB already closed'); } } appInstance.db = new DBUtils(new Database(dbPath)); result = { siteConfig: appInstance.sites[config.name], siteDir: siteDir, authorName: authorName }; event.sender.send('app-site-created', result); }); /* * Regenerate thumbnails */ ipcMain.on('app-site-regenerate-thumbnails', function(event, config) { let site = new Site(appInstance, config, true); self.regenerateProcess = site.regenerateThumbnails(event.sender); }); ipcMain.on('app-site-abort-regenerate-thumbnails', function(event, config) { if (self.regenerateProcess) { self.regenerateProcess.send({ type: 'abort' }); self.regenerateProcess = false; } }); /* * Regenerate thumbnails stauts */ ipcMain.on('app-site-regenerate-thumbnails-required', function(event, config) { let site = new Site(appInstance, config, true); site.regenerateThumbnailsIsRequired(event.sender); }); /* * Delete website */ ipcMain.on('app-site-delete', function (event, config) { Site.delete(appInstance, config.site); delete appInstance.sites[config.site]; event.sender.send('app-site-deleted', true); }); /* * Clone website */ ipcMain.on('app-site-clone', function (event, config) { let clonedWebsiteData = Site.clone(appInstance, config.catalogName, config.siteName); event.sender.send('app-site-cloned', clonedWebsiteData); }); /* * Save custom CSS */ ipcMain.on('app-site-css-save', function (event, config) { Site.saveCustomCSS(appInstance, config.site, config.code); event.sender.send('app-site-css-saved', true); }); /* * Load custom CSS */ ipcMain.on('app-site-css-load', function (event, config) { let customCSS = Site.loadCustomCSS(appInstance, config.site); event.sender.send('app-site-css-loaded', customCSS); }); /* * Check website catalog name */ ipcMain.on('app-site-check-website-to-restore', async function (event, config) { let result = await Site.checkWebsiteBackup(appInstance, config.backupPath); event.sender.send('app-site-backup-checked', result); }); /* * Check website catalog availability */ ipcMain.on('app-site-check-website-catalog-availability', function (event, config) { let result = Site.checkWebsiteCatalogAvailability(appInstance, config.siteName); event.sender.send('app-site-website-catalog-availability-checked', result); }); /* * Remove temp backup files */ ipcMain.on('app-site-remove-temporary-backup-files', function (event, config) { let tempBackupDir = path.join(appInstance.appDir, 'temp', 'backup-to-restore'); if (fs.existsSync(tempBackupDir)) { fs.emptyDirSync(tempBackupDir); } }); /* * Restore website from backup */ ipcMain.on('app-site-restore-from-backup', function (event, config) { let result = Site.restoreFromBackup(appInstance, config.siteName); event.sender.send('app-site-restored-from-backup', result); }); } prepareThemeName(themeName) { if(!themeName) { return false; } return themeName.replace('install-use-', '') .replace('uninstall-', '') .replace('use-', ''); } async loadPassword(settings, type, newPassword) { let account = slug(settings.name); if (settings.uuid) { account = settings.uuid; } if (!settings.deployment.askforpassword || type !== 'publii') { let existingPassword = await passwordSafeStorage.getPassword(type, account); if (newPassword !== '') { if (newPassword === 'publii ' + account) { newPassword = existingPassword; } else { await passwordSafeStorage.setPassword(type, account, newPassword); } } else if (existingPassword !== null) { // Remove the password from the storage if it still exists // and user provided an empty password now await passwordSafeStorage.deletePassword(type, account); } return { newPassword: newPassword, toSave: type + ' ' + account }; } await passwordSafeStorage.deletePassword(type, account); return { newPassword: '', toSave: '' }; } getSiteLanguage (appInstance, siteName) { if (process.platform === 'darwin' || !siteName) { return 'null'; } let configPath = path.join(appInstance.sitesDir, siteName, 'input', 'config', 'site.config.json'); let config = FileHelper.readFileSync(configPath, 'utf8'); try { config = JSON.parse(config); if (config.language) { if (config.language === 'custom') { return config.customLanguage; } return config.language; } } catch (e) { console.log('(!) An error occurred during detecting site language'); } return 'null'; } setSpellcheckerLanguage (appInstance, language) { if (process.platform === 'darwin') { return; } let availableLanguages = appInstance.mainWindow.webContents.session.availableSpellCheckerLanguages; language = language.toLocaleLowerCase(); language = language.split('-'); if (language[1]) { language = language[0] + '-' + language[1].toLocaleUpperCase(); } else { language = language[0]; } if (availableLanguages.indexOf(language) > -1) { appInstance.mainWindow.webContents.session.setSpellCheckerLanguages([language]); console.log('Set spellchecker to:', language); return; } language = language.split('-'); language = language[0]; if (availableLanguages.indexOf(language) > -1) { appInstance.mainWindow.webContents.session.setSpellCheckerLanguages([language]); console.log('Set spellchecker to:', language); return; } console.log('(!) Unable to set spellchecker to use selected language - ' + language); } removeGitConfigDirectory (appInstance, siteName) { let gitDirPath = path.join(appInstance.sitesDir, siteName, 'output', '.git'); if (UtilsHelper.dirExists(gitDirPath)) { fs.removeSync(gitDirPath); } } } module.exports = SiteEvents; ================================================ FILE: app/back-end/events/sync.js ================================================ const fs = require('fs-extra'); const path = require('path'); const FileHelper = require('../helpers/file.js'); const ipcMain = require('electron').ipcMain; /* * Events for the IPC communication regarding sync events */ class SyncEvents { constructor(appInstance) { let self = this; this.app = appInstance; /* * Save information about site that there are no * operations to sync */ ipcMain.on('app-sync-is-done', function (event, config) { self.saveSyncStatus('synced', config.site); event.sender.send('app-sync-is-done-saved', true); }); } saveSyncStatus(status, siteName) { let configFile = path.join(this.app.sitesDir, siteName, 'input', 'config', 'site.config.json'); let configContent = FileHelper.readFileSync(configFile, 'utf8'); configContent = JSON.parse(configContent); configContent.synced = status; if(status === 'synced') { configContent.syncDate = Date.now(); } fs.writeFileSync(configFile, JSON.stringify(configContent, null, 4)); } } module.exports = SyncEvents; ================================================ FILE: app/back-end/events/tag.js ================================================ const ipcMain = require('electron').ipcMain; const Tag = require('../tag.js'); /* * Events for the IPC communication regarding single tags */ class TagEvents { constructor(appInstance) { // Save ipcMain.on('app-tag-save', function (event, tagData) { let tag = new Tag(appInstance, tagData); let result = tag.save(); event.sender.send('app-tag-saved', result); }); // Delete ipcMain.on('app-tag-delete', function (event, tagData) { let result = false; for(let i = 0; i < tagData.ids.length; i++) { let tag = new Tag(appInstance, { site: tagData.site, id: tagData.ids[i] }); result = tag.delete(); } event.sender.send('app-tag-deleted', result); }); // Status change ipcMain.on('app-tags-status-change', function (event, tagData) { let result = false; for(let i = 0; i < tagData.ids.length; i++) { let tag = new Tag(appInstance, { site: tagData.site, id: tagData.ids[i] }); result = tag.changeStatus(tagData.status, tagData.inverse); } event.sender.send('app-tags-status-changed', result); }); // Cancelled edition ipcMain.on('app-tag-cancel', function(event, tagData) { let tag = new Tag(appInstance, tagData); let result = tag.checkAndCleanImages(true); event.sender.send('app-tag-cancelled', result); }); } } module.exports = TagEvents; ================================================ FILE: app/back-end/events/tags.js ================================================ const ipcMain = require('electron').ipcMain; const Posts = require('../posts.js'); const Tags = require('../tags.js'); /* * Events for the IPC communication regarding tags list */ class TagsEvents { constructor(appInstance) { // Load ipcMain.on('app-tags-load', function (event, siteData) { let postsData = new Posts(appInstance, siteData); let tagsData = new Tags(appInstance, siteData); event.sender.send('app-tags-loaded', { tags: tagsData.load(), postsTags: postsData.loadTagsXRef() }); }); } } module.exports = TagsEvents; ================================================ FILE: app/back-end/helpers/app-files.js ================================================ const fs = require('fs-extra'); const path = require('path'); const Utils = require('./utils.js'); /** * Helper class used to manage app-related files */ class AppFilesHelper { /** * Constructor of the helper * * @param appInstance */ constructor(appInstance) { this.application = appInstance; } /** * Relocate all sites from old to new location * * @param oldLocation * @param newLocation * @param event * @returns {boolean} */ relocateSites(oldLocation, newLocation, event) { if(!Utils.dirExists(oldLocation) || !Utils.dirExists(newLocation)) { event.sender.send('app-config-saved', { status: false, message: 'error-save' }); return false; } let sitesToMove = fs.readdirSync(oldLocation); let copyErrorOccurred = false; let catalogsToRemove = []; for(let site of sitesToMove) { let result = this.relocateSite(oldLocation, newLocation, site); if(result === false) { copyErrorOccurred = true; break; } if(result !== '') { catalogsToRemove.push(result); } } if(copyErrorOccurred) { fs.emptyDirSync(newLocation); event.sender.send('app-config-saved', { status: false, message: 'error-save' }); return false; } this.removeCatalogs(oldLocation, catalogsToRemove); this.application.sitesDir = newLocation; this.application.app.sitesDir = newLocation; return true; } /** * Moves specific site files from old to a new location * * @param oldLocation * @param newLocation * @param site * @returns {boolean|string} - false on error, string with site directory name otherwise */ relocateSite(oldLocation, newLocation, site) { let siteLocation = path.join(oldLocation, site); // Checks only for existing directories with Publii website if(!Utils.dirExists(siteLocation) || !this.checkIfDirectoryIsSite(siteLocation)) { return ''; } try { fs.mkdirSync(path.join(newLocation, site), { recursive: true }); fs.copySync( path.join(oldLocation, site), path.join(newLocation, site), { overwrite: true, preserveTimestamps: true } ); } catch(e) { console.log('ERROR OCCURRED: ' + site, e); return false; } return site; } /** * Check if specific directory is a Publii website catalog * * @param siteLocation */ checkIfDirectoryIsSite(siteLocation) { let inputDirPath = path.join(siteLocation, 'input'); let databasePath = path.join(inputDirPath, 'db.sqlite'); let inputDirExists = Utils.dirExists(inputDirPath); let databaseExists = Utils.fileExists(databasePath); return inputDirExists && databaseExists; } /** * Removes specific catalogs * removing all catalogs * * @param catalogsToRemove */ removeCatalogs(location, catalogsToRemove) { for(let catalogToRemove of catalogsToRemove) { let catalogPath = path.join(location, catalogToRemove); fs.removeSync(catalogPath); } } } module.exports = AppFilesHelper; ================================================ FILE: app/back-end/helpers/avatar.js ================================================ const sizeOf = require('image-size'); class AvatarHelper { /* * Check if the provided path is a local avatar file * * @param pathToCheck - avatar path * * @return bool */ static isLocalAvatar(pathToCheck) { return typeof pathToCheck === 'string' && pathToCheck !== '' && pathToCheck.indexOf('http://') === -1 && pathToCheck.indexOf('https://') === -1 && pathToCheck.indexOf('media/website/') === -1; } /* * Check if the provided path is a gravatar remote file * * @param pathToCheck - avatar path * * @return bool */ static isGravatar(pathToCheck) { return !AvatarHelper.isLocalAvatar(pathToCheck); } /* * Returns avatar data object with: * - alt - alternative text (author name) * - url - URL to the avatar image * - width - width of the avatar image * - height - height of the avatar image * * @param authorObject - object with the author data * @param avatarPath - (optional) path to the local avatar image * * @return object - object with the avatar data */ static getAvatarData(authorObject, avatarPath) { let avatarDimensions = { height: 240, width: 240 }; if(avatarPath === '') { return false; } if(avatarPath) { try { avatarDimensions = sizeOf(avatarPath); } catch(e) { console.log('helpers/avatar.js - missing avatar image'); avatarDimensions = { height: false, width: false }; } } return { alt: authorObject.name, height: avatarDimensions.height, url: authorObject.avatar, width: avatarDimensions.width } } } module.exports = AvatarHelper; ================================================ FILE: app/back-end/helpers/context-menu-builder.js ================================================ const electron = require('electron'); const shell = electron.shell; const Menu = electron.Menu; const MenuItem = electron.MenuItem; module.exports = class ContextMenuBuilder { constructor (webContents) { this.menu = null; this.target = webContents; this.translations = { cut: 'Cut', copy: 'Copy', paste: 'Paste', search: 'Search with Google' }; } async showPopupMenu(context) { if (!context.misspelledWord || context.misspelledWord.length === 0) { return; } let menu = await this.buildMenuForElement(context); if (!menu) { return; } menu.popup({}); } async buildMenuForElement(info) { if (info.isEditable || (info.inputFieldType && info.inputFieldType !== 'none')) { return await this.buildMenuForTextInput(info); } } async buildMenuForTextInput(menuInfo) { let menu = new Menu(); await this.addSpellingItems(menu, menuInfo); this.addSearchItems(menu, menuInfo); this.addCut(menu, menuInfo); this.addCopy(menu, menuInfo); this.addPaste(menu, menuInfo); return menu; } async addSpellingItems(menu, menuInfo) { if (!menuInfo.misspelledWord || menuInfo.misspelledWord.length === 0) { return menu; } let corrections = menuInfo.dictionarySuggestions; if (corrections && corrections.length) { corrections.forEach((correction) => { let item = new MenuItem({ label: correction, click: () => this.target.replaceMisspelling(correction) }); menu.append(item); }); this.addSeparator(menu); } return menu; } addSearchItems(menu, menuInfo) { if (!menuInfo.selectionText || menuInfo.selectionText.length < 1) { return menu; } let search = new MenuItem({ label: this.translations.search, click: () => { let url = `https://www.google.com/#q=${encodeURIComponent(menuInfo.selectionText)}`; shell.openExternal(url); } }); menu.append(search); this.addSeparator(menu); return menu; } addCut(menu, menuInfo) { menu.append(new MenuItem({ label: this.translations.cut, accelerator: 'CommandOrControl+X', enabled: menuInfo.editFlags.canCut, click: () => this.target.cut() })); return menu; } addCopy(menu, menuInfo) { menu.append(new MenuItem({ label: this.translations.copy, accelerator: 'CommandOrControl+C', enabled: menuInfo.editFlags.canCopy, click: () => this.target.copy() })); return menu; } addPaste(menu, menuInfo) { menu.append(new MenuItem({ label: this.translations.paste, accelerator: 'CommandOrControl+V', enabled: menuInfo.editFlags.canPaste, click: () => this.target.paste() })); return menu; } addSeparator(menu) { menu.append(new MenuItem({type: 'separator'})); return menu; } } ================================================ FILE: app/back-end/helpers/db.utils.js ================================================ const os = require('os'); /* * Other helper functions */ class DBUtils { constructor (dbInstance) { this.DB = dbInstance; this.statement = ''; this.useWASM = os.platform() === 'linux'; } prepare (sqlStatement) { this.statement = sqlStatement; return this; } get (paramsObject = null) { if (this.useWASM) { if (paramsObject) { paramsObject = this.transformParams(paramsObject); return this.DB.get(this.statement, paramsObject); } return this.DB.get(this.statement); } if (paramsObject !== null) { return this.DB.prepare(this.statement).get(paramsObject); } return this.DB.prepare(this.statement).get(); } run (paramsObject = null) { if (this.useWASM) { if (paramsObject) { paramsObject = this.transformParams(paramsObject); return this.DB.run(this.statement, paramsObject); } return this.DB.run(this.statement); } if (paramsObject !== null) { return this.DB.prepare(this.statement).run(paramsObject); } return this.DB.prepare(this.statement).run(); } all (paramsObject = null) { if (this.useWASM) { if (paramsObject) { paramsObject = this.transformParams(paramsObject); return this.DB.all(this.statement, paramsObject); } return this.DB.all(this.statement); } if (paramsObject !== null) { return this.DB.prepare(this.statement).all(paramsObject); } return this.DB.prepare(this.statement).all(); } exec (sqlQueries) { this.DB.exec(sqlQueries); } close () { this.DB.close(); } /** * Prefix all params in object with "@" */ transformParams (paramsObject) { const newParamsObject = {}; for (const key in paramsObject) { if (paramsObject.hasOwnProperty(key)) { newParamsObject["@" + key] = paramsObject[key]; } } return newParamsObject; } } module.exports = DBUtils; ================================================ FILE: app/back-end/helpers/file.js ================================================ const fs = require('fs'); /** * FileHelper wraps fs.readFileSync to avoid leaking file descriptors. */ class FileHelper { /** * Reads a file synchronously, ensuring the file descriptor is closed. * @param {string|Buffer|URL|integer} path - filename or file descriptor * @param {Object|string} [options] - options or encoding * @returns {string|Buffer} */ static readFileSync(path, options) { let fd; try { fd = fs.openSync(path, 'r'); return fs.readFileSync(fd, options); } finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch (e) { /* ignore */ } } } } } module.exports = FileHelper; ================================================ FILE: app/back-end/helpers/image.helper.js ================================================ /* * Image helper for posts */ const fs = require('fs-extra'); const path = require('path'); const normalizePath = require('normalize-path'); const Utils = require('./utils.js'); const slug = require('./slug'); class ImageHelper { constructor(postInstance) { this.application = postInstance.application; this.site = postInstance.site; this.featuredImage = normalizePath(postInstance.featuredImage); this.featuredImageData = postInstance.featuredImageData; this.fileName = postInstance.featuredImageFilename; this.postID = postInstance.id; this.db = postInstance.db; this.siteDir = postInstance.siteDir; } /* * Save image */ save() { // Detect old image if existed let result = this.db.prepare('SELECT featured_image_id FROM posts WHERE id = @id').get({ id: this.postID }); if (!result) { return; } let featuredImageId = parseInt(result.featured_image_id, 10); // Check if user removed image or image was empty if(this.featuredImage === '' && featuredImageId > 0) { this.delete(); return; } // Store a new image if(this.featuredImage) { // If previous image existed if(featuredImageId > 0) { // Remove them this.delete(); } // And then we can store a new one this.store(); } this.storeImageAdditionalData(); } /* * Delete image */ delete() { if(this.postID > 0) { this.db.prepare(`UPDATE posts SET featured_image_id = 0 WHERE id = @id`).run({ id: this.postID }); this.db.exec(`DELETE FROM posts_images WHERE post_id = ${parseInt(this.postID, 10)}`); } } /* * Store image on the app data */ store() { let self = this; // Check if the image not exist let directoryPath = this.getMediaPath(); let fileNameData = path.parse(this.fileName); let finalFileName = slug(fileNameData.name, false, true) + fileNameData.ext; let finalFilePath = path.join(directoryPath, finalFileName); // Save image data in DB let simplifiedFilePath = normalizePath(finalFilePath).replace(this.getMediaPath(), ''); simplifiedFilePath = simplifiedFilePath.replace('/', '').replace('\\', ''); let imagesSqlQuery = this.db.prepare(`INSERT INTO posts_images VALUES(null, @id, @path, '', '', @data)`); imagesSqlQuery.run({ id: this.postID, path: simplifiedFilePath, data: JSON.stringify(this.featuredImageData) }); let featuredImageId = this.db.prepare('SELECT last_insert_rowid() AS id').get().id; // Update post entry in DB let postsSqlQuery = this.db.prepare(`UPDATE posts SET featured_image_id = @imageID WHERE id = @id`); postsSqlQuery.run({ imageID: featuredImageId, id: this.postID }); } /* * Store image additional data */ storeImageAdditionalData() { // Update featured image entry in DB let imageSqlQuery = this.db.prepare(`UPDATE posts_images SET additional_data = @data WHERE post_id = @id`); imageSqlQuery.run({ data: JSON.stringify(this.featuredImageData), id: this.postID }); } /* * Retrieve media path */ getMediaPath() { let mediaPath = path.join(this.siteDir, 'input', 'media', 'posts', (this.postID).toString()); mediaPath = normalizePath(mediaPath); return mediaPath; } /* * Delete images connected with a specific post ID */ static deleteImagesDirectory(siteDir, itemType, itemID) { let dirPath = path.join(siteDir, 'input', 'media', itemType, (itemID).toString()); let responsiveDirPath = path.join(siteDir, 'input', 'media', itemType, (itemID).toString(), 'responsive'); let galleryDirPath = path.join(siteDir, 'input', 'media', itemType, (itemID).toString(), 'gallery'); for (let directoryPath of [galleryDirPath, responsiveDirPath, dirPath]) { if (Utils.dirExists(directoryPath)) { fs.readdirSync(directoryPath).forEach(function (file) { let curPath = path.join(directoryPath, file); if (!fs.lstatSync(curPath).isDirectory()) { fs.unlinkSync(curPath); } }); fs.rmSync(directoryPath, { recursive: true }); } } } /* * Copy images connected with a specific post ID to other post ID */ static copyImagesDirectory(siteDir, postID, newPostID) { let dirPath = path.join(siteDir, 'input', 'media', 'posts', (postID).toString()); let newDirPath = path.join(siteDir, 'input', 'media', 'posts', (newPostID).toString()); // Copy files from the old directory to the new directory if(Utils.dirExists(dirPath)) { fs.copySync(dirPath, newDirPath); } } } module.exports = ImageHelper; ================================================ FILE: app/back-end/helpers/slug.js ================================================ const transliterate = require('transliteration').transliterate; const slug = require('slug'); /* * Custom mode of rfc3986 without unicode symbols */ slug.defaults.modes['rfc3986-non-unicode'] = { replacement: '-', // replace spaces with replacement symbols: false, // replace unicode symbols or not remove: /[\.]/g, // (optional) regex to remove characters lower: true, // result in lower case charmap: slug.charmap, // replace special characters multicharmap: slug.multicharmap, // replace multi-characters trim: true, // remove leading and trailing replacement chars }; slug.defaults.modes['rfc3986-non-unicode-with-dots'] = { replacement: '-', // replace spaces with replacement symbols: false, // replace unicode symbols or not lower: true, // result in lower case charmap: slug.charmap, // replace special characters multicharmap: slug.multicharmap, // replace multi-characters trim: true, // remove leading and trailing replacement chars }; slug.defaults.modes['rfc3986-non-unicode-with-dots-no-lower'] = { replacement: '-', // replace spaces with replacement symbols: false, // replace unicode symbols or not lower: false, // result in lower case charmap: slug.charmap, // replace special characters multicharmap: slug.multicharmap, // replace multi-characters trim: true, // remove leading and trailing replacement chars }; slug.defaults.mode = 'rfc3986-non-unicode'; /** * Define custom slug charmap */ slug.defaults.charmap['ä'] = 'ae'; slug.defaults.charmap['Ä'] = 'AE'; slug.defaults.charmap['ö'] = 'oe'; slug.defaults.charmap['Ö'] = 'OE'; slug.defaults.charmap['ü'] = 'ue'; slug.defaults.charmap['Ü'] = 'UE'; slug.defaults.charmap['ß'] = 'ss'; slug.defaults.charmap['ẞ'] = 'SS'; function createSlug(textToSlugify, filenameMode = false, saveLowerChars = false) { textToSlugify = transliterate(textToSlugify, { replace: [ ['ä', 'ae'], ['Ä', 'AE'], ['ö', 'oe'], ['Ö', 'OE'], ['ü', 'ue'], ['Ü', 'UE'], ['ß', 'ss'], ['ẞ', 'SS'], ['«', ''], ['»', ''], ['$', ''] ] }); if(!filenameMode) { if(saveLowerChars) { slug.defaults.mode = 'rfc3986-non-unicode-with-dots-no-lower'; } textToSlugify = slug(textToSlugify); slug.defaults.mode = 'rfc3986-non-unicode'; } else { slug.defaults.mode = 'rfc3986-non-unicode-with-dots'; textToSlugify = slug(textToSlugify); slug.defaults.mode = 'rfc3986-non-unicode'; } return textToSlugify; } module.exports = createSlug; ================================================ FILE: app/back-end/helpers/specs/avatar.spec.js ================================================ const assert = require('assert'); const path = require('path'); const avatarHelper = require('../avatar.js'); describe('Avatar Helper', function() { it('should detect if avatar uses local file', function() { assert.strictEqual(false, avatarHelper.isLocalAvatar('')); assert.strictEqual(false, avatarHelper.isLocalAvatar('http://gravatar.com')); assert.strictEqual(false, avatarHelper.isLocalAvatar('https://gravatar.com')); assert.strictEqual(false, avatarHelper.isLocalAvatar('http://domain.com/media/website/img.jpg')); }); it('should detect if avatar uses Gravatar', function() { assert.strictEqual(true, avatarHelper.isGravatar('')); assert.strictEqual(true, avatarHelper.isGravatar('http://gravatar.com')); assert.strictEqual(true, avatarHelper.isGravatar('https://gravatar.com')); assert.strictEqual(true, avatarHelper.isGravatar('http://domain.com/media/website/img.jpg')); }); it('should return avatar object: alt, dimensions, url', function() { const avatarTestPath = path.join(__dirname, 'mock-data', 'avatar.png'); const authorObject = { name: "Test Author", avatar: "http://domain.com/media/website/avatar.png" }; assert.strictEqual(false, avatarHelper.getAvatarData(authorObject, '')); assert.deepEqual({ alt: "Test Author", url: "http://domain.com/media/website/avatar.png", width: 240, height: 127 }, avatarHelper.getAvatarData(authorObject, avatarTestPath)); assert.deepEqual({ alt: "Test Author", url: "http://domain.com/media/website/avatar.png", width: 240, height: 240 }, avatarHelper.getAvatarData(authorObject)); }); }); ================================================ FILE: app/back-end/helpers/specs/slug.spec.js ================================================ const assert = require('assert'); const slug = require('../slug.js'); describe('Slug creation', function() { it('should empty string when there is no chars', function() { assert.strictEqual('', slug(' ')); assert.strictEqual('', slug(" \t \n \r ")); }); it('should remove white spaces', function() { assert.strictEqual('lorem-ipsum', slug('lorem ipsum')); assert.strictEqual('lorem-ipsum', slug('lorem\nipsum')); assert.strictEqual('lorem-ipsum', slug('lorem\tipsum')); assert.strictEqual('lorem-ipsum', slug('lorem\ripsum')); }); it('should change capital letters to lower case', function() { assert.strictEqual('lorem-ipsum', slug('Lorem Ipsum')); }); it('should remove dots from the slug', function() { assert.strictEqual('loremipsum', slug('lorem.ipsum')); assert.strictEqual('loremipsum', slug('lorem.Ipsum')); assert.strictEqual('loremipsum', slug('Lorem.Ipsum')); }); it('should remove special chars from the slug', function() { assert.strictEqual('loremipsum', slug('lorem?!ipsum')); assert.strictEqual('lorem-ipsum', slug('lorem? Ipsum!')); assert.strictEqual('loremipsum', slug('Lorem;Ipsum:+')); }); it('should support japanese chars', function() { assert.strictEqual('konnitihashi-jie', slug('こんにちは世界')); assert.strictEqual('yaa-jin-ri-hayuan-qi-desuka', slug('やあ! 今日は元気ですか?')); assert.strictEqual('yaa-si-noliang-i-sorehazututochang-i-jin-ri-hayuan-qi-desuka', slug('やあ! 私の良い。 それはずっと長い。 今日は元気ですか?')); }); it('should support korean chars', function() { assert.strictEqual('annyeonghaseyo-segye', slug('안녕하세요 세계')); assert.strictEqual('annyeong-cingu-oneul-eoddeoni', slug('안녕 친구! 오늘 어떠니?')); assert.strictEqual('annyeong-cingu-nae-iig-neomu-gilda-oneul-eoddeoni', slug('안녕 친구! 내 이익. 너무 길다. 오늘 어떠니?')); }); it('should support chinese chars', function() { assert.strictEqual('ni-haoshi-jie', slug('你好,世界')); assert.strictEqual('ni-zen-mo-jiao-pei', slug('你怎麼交配')); assert.strictEqual('ni-zen-mo-jiao-pei-wo-hen-hao', slug('你怎麼交配 我很好!')); }); it('should not remove dots in the filename mode', function() { assert.strictEqual('indexhtml', slug('index.html', false)); assert.strictEqual('index.html', slug('index.html', true)); }); it('should remove some special characters', function() { assert.strictEqual('title-with-and-arrows', slug('Title with « and » arrows')); assert.strictEqual('title-with-typoraphical-quotes-and-normal-quotes', slug('Title with „typoraphical quotes“ and "normal quotes"')); assert.strictEqual('title-with-brackets-in-different-forms', slug('Title (with) [brackets] {in} ⟨different forms⟩')); assert.strictEqual('title-with-different-types-of-dashes', slug('Title with different - types – of — dashes')); assert.strictEqual('title-with-many-apostrophes-many-many', slug('Title with many \' apostrophes \‘ many \’ many')); assert.strictEqual('title-with-dots-and-commas', slug('Title with dots . and commas,')); assert.strictEqual('and-another-characters', slug('And another characters ; : ? !')); assert.strictEqual('also-ellipsis', slug('Also ellipsis…')); assert.strictEqual('and-slashes', slug('And slashes \/ \\')); assert.strictEqual('and-other-chars', slug('And other chars * # $ @ ^ % ♥ ☆')); }); }); ================================================ FILE: app/back-end/helpers/updates.helper.js ================================================ const fs = require('fs'); const FileHelper = require('./file.js'); const https = require('https'); class UpdatesHelper { constructor (config) { this.event = config.event; this.filePath = config.filePath; this.url = config.url; this.forceDownload = config.forceDownload; } retrieve () { if (this.forceDownload || !fs.existsSync(this.filePath)) { this.download(); } else { this.readExistingData(); } } download () { https.get(this.url, res => { let body = ''; res.on('data', chunk => { body += chunk; }); res.on('end', () => { fs.writeFileSync(this.filePath, body, 'utf8'); this.handleResponse(body, true); }); }).on('error', (err) => { this.sendError(err); }); } sendError (err) { this.event.sender.send('app-notifications-retrieved', { status: false, error: (err && err.message) ? err.message : 'An unknown error occurred while retrieving notifications.' }); } readExistingData () { if (fs.existsSync(this.filePath)) { let body = FileHelper.readFileSync(this.filePath, 'utf8'); this.handleResponse(body, false); } } handleResponse (body, downloaded) { let response = false; try { response = JSON.parse(body); } catch(e) { response = false; } if (response) { this.event.sender.send('app-notifications-retrieved', { status: true, downloaded: downloaded, notifications: response }); } else { this.sendError(response); } } } module.exports = UpdatesHelper; ================================================ FILE: app/back-end/helpers/utils.js ================================================ const fs = require('fs-extra'); const path = require('path'); const FileHelper = require('./file.js'); const normalizePath = require('normalize-path'); /* * Other helper functions */ class UtilsHelper { /* * * Object helper functions * */ /* * Deep merge for objects as Object.assign not merge objects properly */ static mergeObjects(target, source) { if (typeof target !== 'object') { target = {}; } for (let property in source) { if (source.hasOwnProperty(property)) { let sourceProperty = source[property]; if (typeof sourceProperty === 'object' && !Array.isArray(sourceProperty) && !(sourceProperty instanceof Date)) { target[property] = UtilsHelper.mergeObjects(target[property], sourceProperty); continue; } else if(sourceProperty instanceof Date) { target[property] = new Date(sourceProperty.getTime()); continue; } target[property] = sourceProperty; } } for (let a = 2, l = arguments.length; a < l; a++) { UtilsHelper.mergeObjects(target, arguments[a]); } return target; } /* * * Filesystem helper functions * */ /* * Check if the dir exists */ static dirExists(dirPath) { let dirStat = false; try { dirStat = fs.statSync(dirPath); } catch(e) {} if(dirStat && dirStat.isDirectory()) { return true; } return false; } /* * Check if file exists */ static fileExists(filePath) { let fileStat = false; try { fileStat = fs.statSync(filePath); } catch(e) { return false; } if (fileStat && !fileStat.isDirectory()) { return true; } return false; } /* * * Responsive images helper functions * */ /* * Return true if responsive images config exists */ static responsiveImagesConfigExists(themeConfig, type = false) { let files = themeConfig.files; if(type === false) { return !!files && !!files.responsiveImages && !!files.responsiveImages.contentImages && !!files.responsiveImages.contentImages.dimensions; } // When we want to check if configuration for a specific images exists return !!files && !!files.responsiveImages && !!files.responsiveImages[type] && !!files.responsiveImages[type].dimensions; } /* * Return responsive image dimensions for given config */ static responsiveImagesDimensions(themeConfig, type, group = false) { if(!UtilsHelper.responsiveImagesConfigExists(themeConfig)) { return false; } if(UtilsHelper.responsiveImagesConfigExists(themeConfig, type)) { let dimensions = false; if(themeConfig.files.responsiveImages[type]) { dimensions = themeConfig.files.responsiveImages[type].dimensions; } else { return false; } if(!group) { return UtilsHelper.responsiveImagesDimensionNames(dimensions); } return UtilsHelper.responsiveImagesDimensionNames(dimensions, group); } return false; } /* * Return responsive image dimensions data */ static responsiveImagesData(themeConfig, type, group = false) { if(!UtilsHelper.responsiveImagesConfigExists(themeConfig)) { return false; } if(UtilsHelper.responsiveImagesConfigExists(themeConfig, type)) { let dimensions = false; if(themeConfig.files.responsiveImages[type]) { dimensions = themeConfig.files.responsiveImages[type].dimensions; } else { console.log('TYPE: ' + type + ' NOT EXISTS!'); return false; } let filteredDimensions = false; let dimensionNames = Object.keys(dimensions); if(!group) { return dimensions; } for(let name of dimensionNames) { if(dimensions[name].group.split(',').indexOf(group) > -1) { if(filteredDimensions === false) { filteredDimensions = {}; } filteredDimensions[name] = Object.assign({}, dimensions[name]); } } return filteredDimensions; } return false; } /* * Return responsive images groups */ static responsiveImagesGroups(themeConfig, type) { if (!UtilsHelper.responsiveImagesConfigExists(themeConfig)) { return false; } if (UtilsHelper.responsiveImagesConfigExists(themeConfig, type)) { let groups = false; let dimensions = false; if(themeConfig.files.responsiveImages[type]) { dimensions = themeConfig.files.responsiveImages[type].dimensions; } else { return false; } let keys = Object.keys(dimensions); for(let key of keys) { if(dimensions[key].group) { if(groups === false) { groups = []; } let foundedGroups = dimensions[key].group.split(','); for(let foundedGroup of foundedGroups) { if (groups.indexOf(foundedGroup)) { groups.push(foundedGroup); } } } } return groups; } return false; } /* * Return responsive image dimensions for given config */ static responsiveImagesDimensionNames(dimensions, group = false) { // Get object keys for group type check let keys = Object.keys(dimensions); let dimensionNames = false; // When we have groups and the group param is set - filter results to a specific group if(group !== false) { for(let key of keys) { if(dimensions[key].group.split(',').indexOf(group) > -1) { if(dimensionNames === false) { dimensionNames = []; } dimensionNames.push(key); } } } else { // When there is no groups dimensionNames = Object.keys(dimensions); } return dimensionNames; } /* * Return file when there is no override or file path if the override exists */ static fileIsOverrided(inputDir, themeName, filePath) { let basePath; let overridesDir; let overridedFilePath; if(!filePath) { basePath = normalizePath(path.join(inputDir)); overridesDir = normalizePath(inputDir.replace(/[\\\/]{1,1}$/, '') + '-override'); overridedFilePath = normalizePath(themeName).replace(basePath, overridesDir); } else { basePath = normalizePath(path.join(inputDir, 'themes', themeName)); overridesDir = normalizePath(path.join(inputDir, 'themes', themeName + '-override')); overridedFilePath = normalizePath(filePath).replace(basePath, overridesDir); } if(!UtilsHelper.dirExists(overridesDir)) { return false; } if(!UtilsHelper.fileExists(overridedFilePath)) { return false; } return overridedFilePath; } /* * Loads theme config JSON */ static loadThemeConfig(inputDir, themeName) { let themeConfig; let themeConfigPath; let overridedThemeConfigPath; if(!themeName) { themeConfigPath = path.join(inputDir, 'config.json'); overridedThemeConfigPath = UtilsHelper.fileIsOverrided(inputDir, themeConfigPath); } else { themeName = themeName.toLowerCase(); themeConfigPath = path.join(inputDir, 'themes', themeName, 'config.json'); overridedThemeConfigPath = UtilsHelper.fileIsOverrided(inputDir, themeName, themeConfigPath); } if(overridedThemeConfigPath) { themeConfigPath = overridedThemeConfigPath; } try { themeConfig = JSON.parse(FileHelper.readFileSync(themeConfigPath)); } catch(e) { console.log('The theme config.json file is corrupted'); return {}; } return themeConfig; } /** * Require file without cache */ static requireWithNoCache(module, params = false) { delete require.cache[require.resolve(module)]; if (params) { return require(module)(params); } return require(module); } /** * Compare arrays regardles of order */ static arraysHaveTheSameContent (arrayA, arrayB) { if (arrayA.length !== arrayB.length) { return false; } let uniqueValues = new Set([...arrayA, ...arrayB]); for (let value of uniqueValues) { let arrayACount = arrayA.filter(item => item === value).length; let arrayBCount = arrayB.filter(item => item === value).length; if (arrayACount !== arrayBCount) { return false; } } return true; } } module.exports = UtilsHelper; ================================================ FILE: app/back-end/helpers/validators/language-config.js ================================================ const FileHelper = require('../file.js'); /** * Checks if language config file meets the requirements * * @param configPath - path to the file * @returns {boolean|string} */ function languageConfigValidator(configPath) { let configContent = FileHelper.readFileSync(configPath); let configParsed = false; try { configParsed = JSON.parse(configContent); } catch(e) { return 'Invalid JSON structure'; } if(!configParsed.name) { return 'Missing name field in config.json'; } if(!configParsed.version) { return 'Missing version field in config.json'; } if(!configParsed.author) { return 'Missing author field in config.json'; } if(!configParsed.publiiSupport) { return 'Missing publiiSupport field in config.json'; } return true; } module.exports = languageConfigValidator; ================================================ FILE: app/back-end/helpers/validators/plugin-config.js ================================================ const FileHelper = require('../file.js'); /** * Checks if plugin config file meets the requirements * * @param configPath - path to the file * @returns {boolean|string} */ function pluginConfigValidator(configPath) { let configContent = FileHelper.readFileSync(configPath); let configParsed = false; try { configParsed = JSON.parse(configContent); } catch(e) { return 'Invalid JSON structure'; } if(!configParsed.name) { return 'Missing name field in plugin.json'; } if(!configParsed.version) { return 'Missing version field in plugin.json'; } if(!configParsed.author) { return 'Missing author field in plugin.json'; } if(!configParsed.scope) { return 'Missing scope field in plugin.json'; } if(!configParsed.minimumPubliiVersion) { return 'Missing minimumPubliiVersion field in plugin.json'; } return true; } module.exports = pluginConfigValidator; ================================================ FILE: app/back-end/image.js ================================================ /* * Image instance */ const FileHelper = require('./helpers/file.js'); const fs = require('fs-extra'); const os = require('os'); const path = require('path'); const Model = require('./model.js'); const sizeOf = require('image-size'); const normalizePath = require('normalize-path'); const Themes = require('./themes.js'); const Utils = require('./helpers/utils.js'); const slug = require('./helpers/slug'); const { Jimp } = require('jimp'); // Default config const defaultAstCurrentSiteConfig = require('./../config/AST.currentSite.config'); let sharp = require('sharp'); // Reduce concurrency on Linux systems to avoid crashes if (os.platform() === 'linux') { sharp.concurrency(1); } class Image extends Model { constructor(appInstance, imageData) { super(appInstance, imageData); // Post ID this.id = parseInt(imageData.id, 10); if (imageData.id === 'website') { this.id = 'website'; } else if (imageData.id === 'defaults') { this.id = 'defaults'; } // App instance this.appInstance = appInstance; // Image Path this.path = imageData.path; // Image Type this.imageType = 'contentImages'; // Plugin dir this.pluginDir = imageData.pluginDir; if (imageData.imageType) { this.imageType = imageData.imageType; } } /** * Generate unique file name */ generateFileName (fileName, suffix, dirPath, galleryDirPath) { let newPath = ''; let fileSuffix = ''; let finalFileName = path.parse(fileName); if (suffix > 1) { fileSuffix = '-' + suffix; } finalFileName = slug(finalFileName.name, false, true) + fileSuffix + finalFileName.ext; newPath = path.join(dirPath, finalFileName); if (this.imageType === 'galleryImages') { newPath = path.join(galleryDirPath, finalFileName); } if (fs.existsSync(newPath)) { return this.generateFileName(fileName, suffix + 1, dirPath, galleryDirPath); } return newPath; } /* * Save Image */ save (generateResponsiveImages = true) { let self = this; let newPath = ''; // If image is uploaded to a new post if (this.id === 0) { // Store it in the temp directory this.id = 'temp'; } // For images added to existing posts if (!this.path) { return; } let fileName = this.path.split('/'); if (fileName.length === 1) { fileName = this.path.split('\\'); } fileName = fileName.pop(); // Store the image in the proper directory let dirPath = ''; let galleryDirPath = ''; let responsiveDirPath = ''; if (this.id === 'defaults' && this.imageType === 'contentImages') { dirPath = path.join(this.siteDir, 'input', 'media', 'posts', 'defaults'); responsiveDirPath = path.join(this.siteDir, 'input', 'media', 'posts', 'defaults', 'responsive'); } else if (this.id === 'defaults' && this.imageType === 'tagImages') { dirPath = path.join(this.siteDir, 'input', 'media', 'tags', 'defaults'); responsiveDirPath = path.join(this.siteDir, 'input', 'media', 'tags', 'defaults', 'responsive'); } else if (this.id === 'defaults' && this.imageType === 'authorImages') { dirPath = path.join(this.siteDir, 'input', 'media', 'authors', 'defaults'); responsiveDirPath = path.join(this.siteDir, 'input', 'media', 'authors', 'defaults', 'responsive'); } else if (this.imageType === 'pluginImages') { dirPath = path.join(this.siteDir, 'input', 'media', 'plugins', this.pluginDir); } else if (this.id === 'website' || this.imageType === 'optionImages') { dirPath = path.join(this.siteDir, 'input', 'media', 'website'); responsiveDirPath = path.join(this.siteDir, 'input', 'media', 'website', 'responsive'); } else if (this.imageType === 'tagImages' && this.id) { dirPath = path.join(this.siteDir, 'input', 'media', 'tags', (this.id).toString()); responsiveDirPath = path.join(this.siteDir, 'input', 'media', 'tags', (this.id).toString(), 'responsive'); } else if (this.imageType === 'authorImages' && this.id) { dirPath = path.join(this.siteDir, 'input', 'media', 'authors', (this.id).toString()); responsiveDirPath = path.join(this.siteDir, 'input', 'media', 'authors', (this.id).toString(), 'responsive'); } else { dirPath = path.join(this.siteDir, 'input', 'media', 'posts', (this.id).toString()); responsiveDirPath = path.join(this.siteDir, 'input', 'media', 'posts', (this.id).toString(), 'responsive'); if (this.imageType === 'galleryImages') { galleryDirPath = path.join(this.siteDir, 'input', 'media', 'posts', (this.id).toString(), 'gallery'); } } // If dir not exists - create it if (!Utils.dirExists(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); if (responsiveDirPath !== '') { fs.mkdirSync(responsiveDirPath, { recursive: true }); } } // If gallery directory not exist - create it if (galleryDirPath !== '' && !Utils.dirExists(galleryDirPath)) { fs.mkdirSync(galleryDirPath, { recursive: true }); } // If responsive directory not exist - create it if (responsiveDirPath !== '' && !Utils.dirExists(responsiveDirPath)) { fs.mkdirSync(responsiveDirPath, { recursive: true }); } newPath = this.generateFileName(fileName, 1, dirPath, galleryDirPath); // Store main image try { fs.readFile(this.path, function(err, data) { if (err) throw err; fs.writeFile(newPath, data, function(err) { if (err) throw err; let pathData = path.parse(newPath); // Save responsive images if (generateResponsiveImages && self.allowedImageExtension(pathData.ext)) { self.createResponsiveImages(newPath, self.imageType); } process.send({ type: "image-copied" }); }); }); } catch (err) { return { size: [0, 0], url: 'ERROR' } } // Get image dimensions let dimensions = [false, false]; if (path.parse(this.path).ext === '.svg') { dimensions = this.getSvgImageDimensions(this.path); } else { try { dimensions = sizeOf(this.path); } catch(e) { console.log('back-end/image.js - wrong image path - missing dimensions'); dimensions = [false, false]; } } let filename = path.parse(newPath).base; // Return the image dimensions and new location return { size: [dimensions.width, dimensions.height], url: 'file:///' + normalizePath(newPath), filename: filename, newPath: newPath }; } /* * Save responsive images */ createResponsiveImages(originalPath, imageType = 'contentImages') { let defaultSiteConfig = JSON.parse(JSON.stringify(defaultAstCurrentSiteConfig)); let themesHelper = new Themes(this.application, { site: this.site }); let currentTheme = themesHelper.currentTheme(); let siteConfigPath = path.join(this.siteDir, 'input', 'config', 'site.config.json'); let siteConfig = JSON.parse(FileHelper.readFileSync(siteConfigPath)); siteConfig = Utils.mergeObjects(defaultSiteConfig, siteConfig); let imagesQuality = 60; let alphaQuality = 100; let forceWebp = false; let webpLossless = false; let imageExtension = path.parse(originalPath).ext; let imageDimensions = { width: false, height: false }; if (imageType === 'pluginImages') { return []; } if (!siteConfig.advanced.responsiveImages && imageType !== 'galleryImages') { return ['NO-RESPONSIVE-IMAGES']; } if (!this.allowedImageExtension(imageExtension)) { return []; } try { imageDimensions = sizeOf(this.path); } catch(e) { imageDimensions = { width: false, height: false }; } if ( siteConfig?.advanced?.imagesQuality && !isNaN(parseInt(siteConfig.advanced.imagesQuality, 10)) ) { imagesQuality = siteConfig.advanced.imagesQuality; imagesQuality = parseInt(imagesQuality); if (imagesQuality < 1 || imagesQuality > 100) { imagesQuality = 60; } } if ( siteConfig?.advanced?.alphaQuality && !isNaN(parseInt(siteConfig.advanced.alphaQuality, 10)) ) { alphaQuality = siteConfig.advanced.alphaQuality; alphaQuality = parseInt(alphaQuality); if (alphaQuality < 1 || alphaQuality > 100) { alphaQuality = 100; } } if (siteConfig?.advanced?.webpLossless) { webpLossless = !!siteConfig.advanced.webpLossless; } if (siteConfig?.advanced?.forceWebp && !this.shouldUseJimp()) { forceWebp = !!siteConfig.advanced.forceWebp; } // If there is no selected theme if (currentTheme === 'not selected' && imageType !== 'galleryImages') { return false; } // Load theme config let themeDirPath = path.join(this.siteDir, 'input', 'themes', currentTheme); let themeConfigPath = path.join(this.siteDir, 'input', 'config', 'theme.config.json'); let themeConfig = Themes.loadThemeConfig(themeConfigPath, themeDirPath); let dimensions = false; let dimensionsConfig = false; if (['featuredImages', 'optionImages', 'tagImages', 'authorImages'].indexOf(imageType) > -1) { if (Utils.responsiveImagesConfigExists(themeConfig, imageType)) { dimensions = Utils.responsiveImagesDimensions(themeConfig, imageType); dimensionsConfig = Utils.responsiveImagesData(themeConfig, imageType); } else if (Utils.responsiveImagesConfigExists(themeConfig, 'featuredImages')) { dimensions = Utils.responsiveImagesDimensions(themeConfig, 'featuredImages'); dimensionsConfig = Utils.responsiveImagesData(themeConfig, 'featuredImages'); } else if (Utils.responsiveImagesConfigExists(themeConfig, 'contentImages')) { dimensions = Utils.responsiveImagesDimensions(themeConfig, 'contentImages'); dimensionsConfig = Utils.responsiveImagesData(themeConfig, 'contentImages'); } } else if (imageType === 'contentImages' && Utils.responsiveImagesConfigExists(themeConfig, 'contentImages')) { dimensions = Utils.responsiveImagesDimensions(themeConfig, 'contentImages'); dimensionsConfig = Utils.responsiveImagesData(themeConfig, 'contentImages'); } else if (imageType === 'galleryImages' && Utils.responsiveImagesConfigExists(themeConfig, 'galleryImages')) { dimensions = Utils.responsiveImagesDimensions(themeConfig, 'galleryImages'); dimensionsConfig = Utils.responsiveImagesData(themeConfig, 'galleryImages'); if (!dimensionsConfig) { dimensions = ['thumbnail']; dimensionsConfig = []; dimensionsConfig['thumbnail'] = { crop: true, height: 240, width: 240 }; } } if (!dimensions) { return false; } let targetImagesDir = path.parse(originalPath).dir; if (imageType !== 'galleryImages') { targetImagesDir = path.join(targetImagesDir, 'responsive'); } let promises = []; // create responsive images for each size for (let name of dimensions) { let finalHeight = dimensionsConfig[name].height; let finalWidth = dimensionsConfig[name].width; let cropImage = dimensionsConfig[name].crop; let filename = path.parse(originalPath).name; let extension = path.parse(originalPath).ext; let destinationPath = path.join(targetImagesDir, filename + '-' + name + extension); let result; let shouldBeChangedToWebp = false; if (!this.shouldUseJimp() && ['.png', '.jpg', '.jpeg'].indexOf(extension.toLowerCase()) > -1) { shouldBeChangedToWebp = true; } if (forceWebp && shouldBeChangedToWebp) { destinationPath = path.join(targetImagesDir, filename + '-' + name + '.webp'); } if (!this.allowedImageExtension(extension)) { continue; } if (imageDimensions.width !== false && finalWidth !== 'auto' && finalWidth > imageDimensions.width) { finalWidth = imageDimensions.width; } if (imageDimensions.height !== false && finalHeight !== 'auto' && finalHeight > imageDimensions.height) { finalHeight = imageDimensions.height; } if (finalHeight === 'auto') { finalHeight = null; } if (finalWidth === 'auto') { finalWidth = null; } if (cropImage) { if (this.shouldUseJimp()) { result = new Promise (async (resolve, reject) => { try { let image = await Jimp.read(originalPath); console.log('JIMP COVER', finalWidth, ' x ', finalHeight); if (finalWidth === null || finalHeight === null) { let resizeOptions = {}; if (finalWidth !== null) { resizeOptions.w = finalWidth; } if (finalHeight !== null) { resizeOptions.h = finalHeight; } image.resize(resizeOptions); await image.write(destinationPath, { quality: imagesQuality }); resolve(destinationPath); } else { image.cover({ w: finalWidth, h: finalHeight }); await image.write(destinationPath, { quality: imagesQuality }); resolve(destinationPath); } } catch (err) { console.log(err); reject(err); } }); } else { result = new Promise ((resolve, reject) => { if (extension.toLowerCase() === '.png' && !forceWebp) { sharp(originalPath) .withMetadata() .resize(finalWidth, finalHeight, { withoutEnlargement: true, fastShrinkOnLoad: false }) .toBuffer() .then(function (outputBuffer) { let wstream = fs.createWriteStream(destinationPath); wstream.write(outputBuffer); wstream.end(); resolve(destinationPath); }).catch(err => reject(err)) } else if (extension.toLowerCase() === '.webp' || (forceWebp && shouldBeChangedToWebp)) { let webpConfig = { quality: imagesQuality, alphaQuality: alphaQuality, }; if (webpLossless) { webpConfig = { lossless: true }; } sharp(originalPath) .autoOrient() .withMetadata() .resize(finalWidth, finalHeight, { withoutEnlargement: true, fastShrinkOnLoad: false }) .webp(webpConfig) .toBuffer() .then(function (outputBuffer) { let wstream = fs.createWriteStream(destinationPath); wstream.write(outputBuffer); wstream.end(); resolve(destinationPath); }).catch(err => reject(err)) } else { sharp(originalPath) .withMetadata() .resize(finalWidth, finalHeight, { withoutEnlargement: true, fastShrinkOnLoad: false }) .jpeg({ quality: imagesQuality }) .toBuffer() .then(function (outputBuffer) { let wstream = fs.createWriteStream(destinationPath); wstream.write(outputBuffer); wstream.end(); resolve(destinationPath); }).catch(err => reject(err)) } }).catch(err => console.log(err)); } } else { if (this.shouldUseJimp()) { result = new Promise (async (resolve, reject) => { try { const image = await Jimp.read(originalPath); console.log('JIMP RESIZE/SCALE TO FIT', finalWidth, ' x ', finalHeight); if (finalWidth && finalHeight) { image.scaleToFit({ w: finalWidth, h: finalHeight }); } else if (finalWidth) { image.resize({ w: finalWidth }); } else if (finalHeight) { image.resize({ h: finalHeight }); } await image.write(destinationPath, { quality: imagesQuality }); resolve(destinationPath); } catch (err) { console.error(err); reject(err); } }); } else { result = new Promise ((resolve, reject) => { if (extension.toLowerCase() === '.png' && !forceWebp) { sharp(originalPath) .withMetadata() .resize(finalWidth, finalHeight, { fit: 'inside', withoutEnlargement: true, fastShrinkOnLoad: false }) .toBuffer() .then(function (outputBuffer) { let wstream = fs.createWriteStream(destinationPath); wstream.write(outputBuffer); wstream.end(); resolve(destinationPath); }).catch(err => reject(err)); } else if (extension.toLowerCase() === '.webp' || (forceWebp && shouldBeChangedToWebp)) { let webpConfig = { quality: imagesQuality, alphaQuality: alphaQuality, }; if (webpLossless) { webpConfig = { lossless: true }; } sharp(originalPath) .autoOrient() .withMetadata() .resize(finalWidth, finalHeight, { fit: 'inside', withoutEnlargement: true, fastShrinkOnLoad: false }) .webp(webpConfig) .toBuffer() .then(function (outputBuffer) { let wstream = fs.createWriteStream(destinationPath); wstream.write(outputBuffer); wstream.end(); resolve(destinationPath); }).catch(err => reject(err)); } else { sharp(originalPath) .withMetadata() .resize(finalWidth, finalHeight, { fit: 'inside', withoutEnlargement: true, fastShrinkOnLoad: false }) .jpeg({ quality: imagesQuality }) .toBuffer() .then(function (outputBuffer) { let wstream = fs.createWriteStream(destinationPath); wstream.write(outputBuffer); wstream.end(); resolve(destinationPath); }).catch(err => reject(err)); } }).catch(err => console.log(err)); } } promises.push(result); } return promises; } /* * Get SVG image dimensions */ getSvgImageDimensions(imagePath) { let result = { height: false, width: false }; // Get content of the SVG image let svgFileContent = FileHelper.readFileSync(imagePath, 'utf8'); // Look for the non-percentage values in the tag let svgWidth = svgFileContent.match(/\/mi); let svgHeight = svgFileContent.match(/\/mi); let svgViewBox = svgFileContent.match(/\/mi); if (svgWidth && svgHeight && svgWidth[1].indexOf('%') === 1 && svgHeight[1].indexOf('%') === 1) { result.height = parseInt(svgHeight, 10); result.width = parseInt(svgWidth, 10); } else if (svgViewBox && svgViewBox[1]) { svgViewBox = svgViewBox[1].split(' '); if (svgViewBox.length === 4) { result.height = svgViewBox[3]; result.width = svgViewBox[2]; } } return result; } /* * Check if the image has supported image extension */ allowedImageExtension(extension) { let allowedExtensions = ['.jpg', '.jpeg', '.png', '.JPG', '.JPEG', '.PNG', '.webp', '.WEBP']; if (this.shouldUseJimp()) { allowedExtensions = ['.jpg', '.jpeg', '.png', '.JPG', '.JPEG', '.PNG']; } return allowedExtensions.indexOf(extension) > -1; } /* * Detect if Jimp should be used */ shouldUseJimp() { return this.appInstance.appConfig.resizeEngine && this.appInstance.appConfig.resizeEngine === 'jimp'; } } module.exports = Image; ================================================ FILE: app/back-end/languages.js ================================================ /* * Languages instance */ const fs = require('fs-extra'); const path = require('path'); const FileHelper = require('./helpers/file.js'); const UtilsHelper = require('./helpers/utils.js'); const languageConfigValidator = require('./helpers/validators/language-config.js'); const normalizePath = require('normalize-path'); class Languages { constructor(appInstance) { this.basePath = appInstance.appDir; this.languagesPath = path.join(this.basePath, 'languages'); this.appInstance = appInstance; } /* * Load languages from a specific path */ loadLanguages () { let pathToLanguages = this.languagesPath; let pathToDefaultLanguages = path.join(__dirname, '..', 'default-files', 'default-languages').replace('app.asar', 'app.asar.unpacked'); let output = []; // Load default languages let defaultLanguages = fs.readdirSync(pathToDefaultLanguages); for(let i = 0; i < defaultLanguages.length; i++) { if (defaultLanguages[i][0] === '.' || !UtilsHelper.dirExists(path.join(pathToDefaultLanguages, defaultLanguages[i]))) { continue; } let configPath = path.join(pathToDefaultLanguages, defaultLanguages[i], 'config.json'); // Load only proper languages if (!fs.existsSync(configPath)) { continue; } // Load only properly configured languages if(languageConfigValidator(configPath) !== true) { continue; } let languageData = FileHelper.readFileSync(configPath, 'utf8'); languageData = JSON.parse(languageData); output.push({ type: 'default', directory: defaultLanguages[i], name: languageData.name, version: languageData.version, author: languageData.author, publiiSupport: languageData.publiiSupport, momentLocale: languageData.momentLocale, wysiwygTranslation: languageData.wysiwygTranslation }); } // Load additional languages let filesAndDirs = fs.readdirSync(pathToLanguages); for(let i = 0; i < filesAndDirs.length; i++) { if (filesAndDirs[i][0] === '.' || !UtilsHelper.dirExists(path.join(pathToLanguages, filesAndDirs[i]))) { continue; } let configPath = path.join(pathToLanguages, filesAndDirs[i], 'config.json'); // Load only proper languages if (!fs.existsSync(configPath)) { continue; } // Load only properly configured languages if(languageConfigValidator(configPath) !== true) { continue; } let languageData = FileHelper.readFileSync(configPath, 'utf8'); languageData = JSON.parse(languageData); output.push({ type: 'installed', directory: filesAndDirs[i], name: languageData.name, version: languageData.version, author: languageData.author, publiiSupport: languageData.publiiSupport, momentLocale: languageData.momentLocale, wysiwygTranslation: languageData.wysiwygTranslation }); } return output; } /* * Remove specific language from the app directory */ removeLanguage(directory) { fs.removeSync(path.join(this.languagesPath, directory)); } /* * Fixes path for the media file */ normalizeLanguageImagePath(imagePath) { // Save the image if necessary imagePath = normalizePath(imagePath); imagePath = imagePath.replace('file:/', ''); return imagePath; } /** * Load translations */ loadTranslations (languageName = 'en-gb', type = 'default') { let translationsPath = path.join(__dirname, '..', 'default-files', 'default-languages').replace('app.asar', 'app.asar.unpacked'); if (type !== 'default') { translationsPath = this.languagesPath; } translationsPath = path.join(translationsPath, languageName, 'translations.json'); if (!UtilsHelper.fileExists(translationsPath)) { return false; } return UtilsHelper.requireWithNoCache(translationsPath); } /** * Load translations */ loadWysiwygTranslation (languageName = 'en-gb', type = 'default') { let translationsPath = path.join(__dirname, '..', 'default-files', 'default-languages').replace('app.asar', 'app.asar.unpacked'); if (type !== 'default') { translationsPath = this.languagesPath; } translationsPath = path.join(translationsPath, languageName, 'wysiwyg.json'); if (!UtilsHelper.fileExists(translationsPath)) { return false; } return UtilsHelper.requireWithNoCache(translationsPath); } /** * Load language config */ loadLanguageConfig (languageName = 'en-gb', type = 'default') { let configPath = path.join(__dirname, '..', 'default-files', 'default-languages').replace('app.asar', 'app.asar.unpacked'); if (type !== 'default') { configPath = this.languagesPath; } configPath = path.join(configPath, languageName, 'config.json'); if (!UtilsHelper.fileExists(configPath)) { return false; } let languageConfig = FileHelper.readFileSync(configPath, 'utf8'); try { languageConfig = JSON.parse(languageConfig); } catch (e) { return false; } return languageConfig; } } module.exports = Languages; ================================================ FILE: app/back-end/migrators/site-config.js ================================================ const fs = require('fs'); const path = require('path'); const os = require('os'); const slug = require('./../helpers/slug'); const Database = os.platform() === 'linux' ? require('node-sqlite3-wasm').Database : require('better-sqlite3'); const DBUtils = require('../helpers/db.utils.js'); class SiteConfigMigrator { static moveOldAuthorData(appInstance, siteConfig) { // Check if old author data exists if(!siteConfig.author && siteConfig.author !== null) { // Return unmodified site config return siteConfig; } console.log('MIRATION IN: ' + siteConfig.name); // If yes - save them in database as author with ID = 1 let siteDir = path.join(appInstance.sitesDir, siteConfig.name); let dbPath = path.join(siteDir, 'input', 'db.sqlite'); let db = new DBUtils(new Database(dbPath)); let newAuthorName = siteConfig.author.name; let newAuthorUsername = slug(newAuthorName); let newAuthorConfig = { avatar: siteConfig.author.avatar, email: siteConfig.author.email, description: siteConfig.author.description, useGravatar: siteConfig.author.useGravatar }; newAuthorConfig = JSON.stringify(newAuthorConfig); let configFilePath = path.join(siteDir, 'input', 'config', 'site.config.json'); let sqlQuery = db.prepare(` UPDATE authors SET name = @name, username = @username, config = @config WHERE id = 1; `); sqlQuery.run({ name: newAuthorName, username: newAuthorUsername, config: newAuthorConfig }); // Remove from the config author data delete siteConfig.author; fs.writeFileSync(configFilePath, JSON.stringify(siteConfig, null, 4), {'flags': 'w'}); // close DB connection db.close(); // Return modified (or not) site config return siteConfig; } } module.exports = SiteConfigMigrator; ================================================ FILE: app/back-end/model.js ================================================ /* * Model base class */ const fs = require('fs'); const path = require('path'); class Model { /** * Model constructor * * @param appInstance * @param data */ constructor(appInstance, data) { this.application = appInstance; this.db = this.application.db; this.site = data.site; this.appDir = this.application.appDir; this.siteDir = path.join(this.application.sitesDir, this.site); this.dbPath = path.join(this.siteDir, 'input', 'db.sqlite'); } /** * Escapes given string * * @param stringToEscape * @returns {*} */ escape(stringToEscape) { if(stringToEscape == '') { return stringToEscape; } return stringToEscape .replace(/\\/g, "\\\\") .replace(/\'/g, "\\\'") .replace(/\"/g, "\\\"") .replace(/\n/g, "\\\n") .replace(/\r/g, "\\\r") .replace(/\x00/g, "\\\x00") .replace(/\x1a/g, "\\\x1a"); } /** * Modify field * * @param {string} table - table name * @param {number} itemID - item ID * @param {Array} fieldsToUpdate - array of fields to update * * [{ * field: 'slug', * value: 'new-slug-value', * type: 'field' * }, * { * field: '_core', * subfield: 'metaDesc' * value: 'New Title', * type: 'json' * }] */ updateField (table, itemID, fieldsToUpdate) { const ALLOWED_TABLES = [ 'authors', 'posts', 'posts_additional_data', 'tags' ]; const ALLOWED_COLUMNS_BY_TABLE = { 'authors': ['name', 'username', 'config', 'additional_data'], 'posts': ['slug', 'title', 'text'], 'posts_additional_data': ['key', 'value'], 'tags': ['name', 'slug', 'description', 'additional_data'] }; if (!ALLOWED_TABLES.includes(table)) { return { status: 'error', error: 'Invalid table name: ' + table }; } let invalidField = false; fieldsToUpdate.forEach(fieldObj => { if (invalidField) { return; } let dbColumnName; let tableName = table; let idColumn = 'id'; if (table === 'posts' && fieldObj.field === '_core') { tableName = 'posts_additional_data'; } if (tableName === 'posts_additional_data') { idColumn = 'post_id'; } let allowedFieldsForThisTable = ALLOWED_COLUMNS_BY_TABLE[tableName] || []; if (tableName === 'posts_additional_data') { dbColumnName = 'value'; } else { dbColumnName = fieldObj.field; } if (!allowedFieldsForThisTable.includes(dbColumnName)) { invalidField = dbColumnName; return; } if (fieldObj.type === 'field') { let sql = this.db.prepare(`UPDATE ${tableName} SET ${dbColumnName} = @column WHERE ${idColumn} = @id`); sql.run({ column: fieldObj.value, id: itemID }); } else if (fieldObj.type === 'json') { let sqlSelect; let sqlUpdate; let jsonData; if (table === 'posts_additional_data') { sqlSelect = this.db.prepare(`SELECT value FROM ${tableName} WHERE ${idColumn} = @id AND key = @field`); let row = sqlSelect.get({ id: itemID, field: fieldObj.field }); jsonData = row ? JSON.parse(row.value) : {}; } else { sqlSelect = this.db.prepare(`SELECT ${dbColumnName} FROM ${tableName} WHERE ${idColumn} = @id`); let row = sqlSelect.get({ id: itemID }); jsonData = row ? JSON.parse(row[dbColumnName]) : {}; } jsonData[fieldObj.subfield] = fieldObj.value; let newJsonString = JSON.stringify(jsonData); if (table === 'posts_additional_data') { sqlUpdate = this.db.prepare(`UPDATE ${tableName} SET value = @json WHERE ${idColumn} = @id AND key = @field`); sqlUpdate.run({ json: newJsonString, id: itemID, field: fieldObj.field }); } else { sqlUpdate = this.db.prepare(`UPDATE ${tableName} SET ${dbColumnName} = @json WHERE ${idColumn} = @id AND key = @field`); sqlUpdate.run({ json: newJsonString, id: itemID, field: fieldObj.field }); } } }); if (invalidField) { return { status: 'error', error: 'Invalid table column name: ' + invalidField }; } return { status: 'success' }; } } module.exports = Model; ================================================ FILE: app/back-end/modules/backup/backup.js ================================================ /* * Class used to create backups */ const fs = require('fs-extra'); const path = require('path'); const FileHelper = require('./../../helpers/file.js'); const Utils = require('./../../helpers/utils.js'); const moment = require('moment'); const archiver = require('archiver'); const tar = require('tar-fs'); const { shell } = require('electron'); class Backup { /** * Loads list of backups * * @param siteName * @param backupsDir * @returns {*} */ static loadList(siteName, backupsDir) { let backupsPath = path.join(backupsDir, siteName); if(!Utils.dirExists(backupsPath)) { if(Utils.dirExists(backupsDir)) { fs.mkdirSync(backupsPath, { recursive: true }); } else { return false; } } let files = []; let allFiles = fs.readdirSync(backupsPath); let index = 0; for(let file of allFiles) { if(path.parse(file).ext !== '.tar') { continue; } let stats = fs.statSync(path.join(backupsPath, file)); let size = Backup.convertToMegabytes(stats.size); let createdAt = stats.birthtime || stats.mtime; files.push({ id: index, name: file, size: size, url: path.join(backupsPath, file), createdAt: Date.parse(createdAt) }); index++; } files.sort((a,b) => { return b.createdAt - a.createdAt; }); files = files.map(item => { item.createdAt = moment(new Date(item.createdAt)).format('MM-DD-YYYY HH:mm'); return item; }); return files; } /** * Creates backup * * @param siteName * @param backupFilename * @param backupsDir * @param sourceDir */ static async create(siteName, backupFilename, backupsDir, sourceDir) { let sourcePath = path.join(sourceDir); let backupsPath = path.join(backupsDir, siteName); if(!Utils.dirExists(backupsPath)) { if(Utils.dirExists(backupsDir)) { fs.mkdirSync(backupsPath, { recursive: true }); } else { return { type: 'app-backup-create-error', status: false, error: 'core.backup.locationDoesNotExists' }; } } if (Utils.dirExists(backupsPath)) { backupFilename = backupFilename.replace(/[^a-z0-9\-\_]/gmi, ''); let backupFile = path.join(backupsPath, backupFilename + '.tar'); let createOperation = new Promise(function (resolve, reject) { let output = fs.createWriteStream(backupFile); let archive = archiver('tar'); output.on('error', function (err) { resolve({ type: 'app-backup-create-error', status: false, error: err }); }); output.on('close', function () { resolve({ type: 'app-backup-create-success', backups: Backup.loadList(siteName, backupsDir) }); }); archive.on('error', function (err) { resolve({ type: 'app-backup-create-error', status: false, error: err }); }); archive.pipe(output); archive.append((+new Date).toString(), { name: 'backup-date.log' }); archive.directory(sourcePath + '/input/', 'input'); archive.finalize(); }); let results = await createOperation; return results; } return { type: 'app-backup-create-error', status: false, error: 'core.backup.locationDoesNotExists' }; } /** * Removes backup * * @param siteName * @param backupsNames * @param backupsDir * @returns {{status: boolean, backups: *}} */ static async remove(siteName, backupsNames, backupsDir) { for(let backupName of backupsNames) { let backupFilePath = path.join(backupsDir, siteName, backupName); if (!Utils.fileExists(backupFilePath)) { return { status: false, backups: Backup.loadList(siteName, backupsDir) }; } try { await shell.trashItem(backupFilePath); } catch (e) { console.log('ERR:', e); return Promise.resolve({ status: false, backups: Backup.loadList(siteName, backupsDir) }); } } return Promise.resolve({ status: true, backups: Backup.loadList(siteName, backupsDir) }); } /** * Renames backup * * @param siteName * @param oldBackupName * @param newBackupName * @param backupsDir * @returns {{status: boolean, backups: *}} */ static rename(siteName, oldBackupName, newBackupName, backupsDir) { let oldBackupFilePath = path.join(backupsDir, siteName, oldBackupName + '.tar'); let newBackupFilePath = path.join(backupsDir, siteName, newBackupName + '.tar'); if (!Utils.fileExists(oldBackupFilePath) || Utils.fileExists(newBackupFilePath)) { return { status: false, backups: Backup.loadList(siteName, backupsDir) }; } try { fs.renameSync(oldBackupFilePath, newBackupFilePath); } catch (e) { return { status: false, backups: Backup.loadList(siteName, backupsDir) }; } return { status: true, backups: Backup.loadList(siteName, backupsDir) }; } /** * Restores backup * * @param siteName * @param backupName * @param backupsDir * @param destinationDir * @param tempDir */ static async restore(siteName, backupName, backupsDir, destinationDir, tempDir, appInstance) { let backupFilePath = path.join(backupsDir, siteName, backupName); let destinationPath = path.join(destinationDir, siteName); if (!Utils.fileExists(backupFilePath)) { return { type: 'app-backup-restore-error', status: false, error: 'core.backup.fileDoesNotExists' }; } if (!Utils.dirExists(destinationDir)) { return { type: 'app-backup-restore-error', status: false, error: 'core.backup.destinationDirectoryDoesNotExists' }; } if(!Utils.dirExists(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); if(!Utils.dirExists(tempDir)) { return { type: 'app-backup-restore-error', status: false, error: 'core.backup.temporaryDirectoryDoesNotExists' }; } } // Empty the temp directory before extracting the backups content fs.emptyDirSync(tempDir); let restoreOperation = new Promise(function (resolve, reject) { fs.createReadStream(backupFilePath) .on('error', function(err) { resolve({ type: 'app-backup-restore-error', status: false, error: 'core.backup.errorDuringReadingBackupFile' }); }) .pipe(tar.extract(tempDir, { finish: () => { // Verify the backup let backupTest = Backup.verify(tempDir, siteName); if(!backupTest) { resolve({ type: 'app-backup-restore-error', status: false, error: 'core.backup.errorDuringReadingBackupFile' }); return; } // Close DB connection and remove site dir contents if (appInstance.db) { try { appInstance.db.close(); } catch (e) { console.log('[BACKUP RESTORE] DB already closed'); } } fs.emptyDirSync(destinationPath); // Move files from the temp dir to the site dir let backupContents = fs.readdirSync(tempDir); for(let content of backupContents) { fs.moveSync( path.join(tempDir, content), path.join(destinationPath, content) ); } resolve({ type: 'app-backup-restore-success', status: true }); }})); }); let results = await restoreOperation; return results; } /** * Verifies backup * * @param backupDir * @param siteName * @returns {boolean} */ static verify(backupDir, siteName) { let foundedErrors = false; let configFilePath = path.join(backupDir, 'input', 'config', 'site.config.json'); let dirsToCheck = [ path.join(backupDir, 'input'), path.join(backupDir, 'input', 'config'), path.join(backupDir, 'input', 'media'), path.join(backupDir, 'input', 'themes'), ]; let filesToCheck = [ path.join(backupDir, 'input', 'db.sqlite'), configFilePath ]; for(let i = 0; i < dirsToCheck.length; i++) { if (!Utils.dirExists(dirsToCheck[i])) { foundedErrors = true; } } for(let i = 0; i < filesToCheck.length; i++) { if (!Utils.fileExists(filesToCheck[i])) { foundedErrors = true; } } // If errors were founded if(foundedErrors) { parentPort.postMessage({ type: 'app-backup-restore-error', status: false, error: 'core.backup.fileIsCorrupted' }); return false; } Backup.checkSiteName(siteName, configFilePath); return true; } /** * * Check if the site name in the config file is the same as the current site name * * if not - change the config before the backup restore * * @param siteName - name of the website to check * @param configFilePath - path to the temporary config file * */ static checkSiteName(siteName, configFilePath) { let configContent = FileHelper.readFileSync(configFilePath); try { configContent = JSON.parse(configContent); if(configContent.name !== siteName) { configContent.name = siteName; fs.writeFileSync(configFilePath, JSON.stringify(configContent, null, 4)); } } catch(e) { console.log('modules/backup.js: Wrong site.config.json file'); } } /** * Converts bytes to megabytes * * @param fileSizeInBytes * @returns {string} */ static convertToMegabytes(fileSizeInBytes) { return Number(fileSizeInBytes / (1024 * 1024)).toFixed(2) + ' MB'; } } module.exports = Backup; ================================================ FILE: app/back-end/modules/backup/create-from-backup.js ================================================ const fs = require('fs-extra'); const path = require('path'); const FileHelper = require('./../../helpers/file.js'); const tar = require('tar-fs'); const Utils = require('./../../helpers/utils.js'); class CreateFromBackup { constructor (appInstance, backupPath) { this.backupPath = backupPath; this.appInstance = appInstance; this.baseDir = path.join(this.appInstance.appDir, 'temp'); this.tempDir = path.join(this.baseDir, 'backup-to-restore'); } async prepareBackupToRestore () { if (!this.checkExtension()) { return { status: 'error', type: 'unsupported-format' }; } return await this.unpackBackup(); } checkExtension () { if (this.backupPath.substr(-4) === '.tar') { return true; } return false; } async unpackBackup () { this.removeBackupFilesIfNecessary(); let extractOperation = new Promise((resolve, reject) => { fs.createReadStream(this.backupPath).on('error', (err) => { this.removeBackupFilesIfNecessary(); resolve({ status: 'error', type: 'unpack-error' }); }).pipe(tar.extract(this.tempDir, { finish: () => { let backupTestResult = this.verifyBackup(this.tempDir); if (!backupTestResult) { this.removeBackupFilesIfNecessary(); resolve({ status: 'error', type: 'invalid-backup-content' }); return; } let siteNameData = this.getSiteName(); if (!siteNameData) { this.removeBackupFilesIfNecessary(); resolve({ status: 'error', type: 'invalid-site-data' }); return; } resolve({ status: 'success', type: 'unpack-success', data: { displayName: siteNameData.displayName, catalogName: siteNameData.catalogName } }); }})); }); let results = await extractOperation; return results; } verifyBackup(backupDir) { let foundedErrors = false; let configFilePath = path.join(backupDir, 'input', 'config', 'site.config.json'); let dirsToCheck = [ path.join(backupDir, 'input'), path.join(backupDir, 'input', 'config'), path.join(backupDir, 'input', 'media'), path.join(backupDir, 'input', 'themes'), ]; let filesToCheck = [ path.join(backupDir, 'input', 'db.sqlite'), configFilePath ]; for(let i = 0; i < dirsToCheck.length; i++) { if (!Utils.dirExists(dirsToCheck[i])) { foundedErrors = true; } } for(let i = 0; i < filesToCheck.length; i++) { if (!Utils.fileExists(filesToCheck[i])) { foundedErrors = true; } } // If errors were founded if(foundedErrors) { return false; } return true; } getSiteName () { let configFilePath = path.join(this.tempDir, 'input', 'config', 'site.config.json'); let configContent = FileHelper.readFileSync(configFilePath, 'utf8'); let siteNameData = false; try { let parsedConfig = JSON.parse(configContent); siteNameData = { displayName: parsedConfig.displayName, catalogName: parsedConfig.name }; } catch (e) { siteNameData = false; } return siteNameData; } removeBackupFilesIfNecessary () { if (fs.existsSync(this.tempDir)) { fs.emptyDirSync(this.tempDir); } } } module.exports = CreateFromBackup; ================================================ FILE: app/back-end/modules/custom-changes/ftp/LICENSE ================================================ Copyright Brian White. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: app/back-end/modules/custom-changes/ftp/README.md ================================================ Description =========== node-ftp is an FTP client module for [node.js](http://nodejs.org/) that provides an asynchronous interface for communicating with an FTP server. Requirements ============ * [node.js](http://nodejs.org/) -- v0.8.0 or newer Install ======= npm install ftp Examples ======== * Get a directory listing of the current (remote) working directory: ```javascript var Client = require('ftp'); var c = new Client(); c.on('ready', function() { c.list(function(err, list) { if (err) throw err; console.dir(list); c.end(); }); }); // connect to localhost:21 as anonymous c.connect(); ``` * Download remote file 'foo.txt' and save it to the local file system: ```javascript var Client = require('ftp'); var fs = require('fs'); var c = new Client(); c.on('ready', function() { c.get('foo.txt', function(err, stream) { if (err) throw err; stream.once('close', function() { c.end(); }); stream.pipe(fs.createWriteStream('foo.local-copy.txt')); }); }); // connect to localhost:21 as anonymous c.connect(); ``` * Upload local file 'foo.txt' to the server: ```javascript var Client = require('ftp'); var fs = require('fs'); var c = new Client(); c.on('ready', function() { c.put('foo.txt', 'foo.remote-copy.txt', function(err) { if (err) throw err; c.end(); }); }); // connect to localhost:21 as anonymous c.connect(); ``` API === Events ------ * **greeting**(< _string_ >msg) - Emitted after connection. `msg` is the text the server sent upon connection. * **ready**() - Emitted when connection and authentication were sucessful. * **close**(< _boolean_ >hadErr) - Emitted when the connection has fully closed. * **end**() - Emitted when the connection has ended. * **error**(< _Error_ >err) - Emitted when an error occurs. In case of protocol-level errors, `err` contains a 'code' property that references the related 3-digit FTP response code. Methods ------- **\* Note: As with the 'error' event, any error objects passed to callbacks will have a 'code' property for protocol-level errors.** * **(constructor)**() - Creates and returns a new FTP client instance. * **connect**(< _object_ >config) - _(void)_ - Connects to an FTP server. Valid config properties: * host - _string_ - The hostname or IP address of the FTP server. **Default:** 'localhost' * port - _integer_ - The port of the FTP server. **Default:** 21 * secure - _mixed_ - Set to true for both control and data connection encryption, 'control' for control connection encryption only, or 'implicit' for implicitly encrypted control connection (this mode is deprecated in modern times, but usually uses port 990) **Default:** false * secureOptions - _object_ - Additional options to be passed to `tls.connect()`. **Default:** (none) * user - _string_ - Username for authentication. **Default:** 'anonymous' * password - _string_ - Password for authentication. **Default:** 'anonymous@' * connTimeout - _integer_ - How long (in milliseconds) to wait for the control connection to be established. **Default:** 10000 * pasvTimeout - _integer_ - How long (in milliseconds) to wait for a PASV data connection to be established. **Default:** 10000 * keepalive - _integer_ - How often (in milliseconds) to send a 'dummy' (NOOP) command to keep the connection alive. **Default:** 10000 * **end**() - _(void)_ - Closes the connection to the server after any/all enqueued commands have been executed. * **destroy**() - _(void)_ - Closes the connection to the server immediately. ### Required "standard" commands (RFC 959) * **list**([< _string_ >path, ][< _boolean_ >useCompression, ]< _function_ >callback) - _(void)_ - Retrieves the directory listing of `path`. `path` defaults to the current working directory. `useCompression` defaults to false. `callback` has 2 parameters: < _Error_ >err, < _array_ >list. `list` is an array of objects with these properties: * type - _string_ - A single character denoting the entry type: 'd' for directory, '-' for file (or 'l' for symlink on **\*NIX only**). * name - _string_ - The name of the entry. * size - _string_ - The size of the entry in bytes. * date - _Date_ - The last modified date of the entry. * rights - _object_ - The various permissions for this entry **(*NIX only)**. * user - _string_ - An empty string or any combination of 'r', 'w', 'x'. * group - _string_ - An empty string or any combination of 'r', 'w', 'x'. * other - _string_ - An empty string or any combination of 'r', 'w', 'x'. * owner - _string_ - The user name or ID that this entry belongs to **(*NIX only)**. * group - _string_ - The group name or ID that this entry belongs to **(*NIX only)**. * target - _string_ - For symlink entries, this is the symlink's target **(*NIX only)**. * sticky - _boolean_ - True if the sticky bit is set for this entry **(*NIX only)**. * **get**(< _string_ >path, [< _boolean_ >useCompression, ]< _function_ >callback) - _(void)_ - Retrieves a file at `path` from the server. `useCompression` defaults to false. `callback` has 2 parameters: < _Error_ >err, < _ReadableStream_ >fileStream. * **put**(< _mixed_ >input, < _string_ >destPath, [< _boolean_ >useCompression, ]< _function_ >callback) - _(void)_ - Sends data to the server to be stored as `destPath`. `input` can be a ReadableStream, a Buffer, or a path to a local file. `useCompression` defaults to false. `callback` has 1 parameter: < _Error_ >err. * **append**(< _mixed_ >input, < _string_ >destPath, [< _boolean_ >useCompression, ]< _function_ >callback) - _(void)_ - Same as **put()**, except if `destPath` already exists, it will be appended to instead of overwritten. * **rename**(< _string_ >oldPath, < _string_ >newPath, < _function_ >callback) - _(void)_ - Renames `oldPath` to `newPath` on the server. `callback` has 1 parameter: < _Error_ >err. * **logout**(< _function_ >callback) - _(void)_ - Logout the user from the server. `callback` has 1 parameter: < _Error_ >err. * **delete**(< _string_ >path, < _function_ >callback) - _(void)_ - Deletes a file, `path`, on the server. `callback` has 1 parameter: < _Error_ >err. * **cwd**(< _string_ >path, < _function_ >callback) - _(void)_ - Changes the current working directory to `path`. `callback` has 2 parameters: < _Error_ >err, < _string_ >currentDir. Note: `currentDir` is only given if the server replies with the path in the response text. * **abort**(< _function_ >callback) - _(void)_ - Aborts the current data transfer (e.g. from get(), put(), or list()). `callback` has 1 parameter: < _Error_ >err. * **site**(< _string_ >command, < _function_ >callback) - _(void)_ - Sends `command` (e.g. 'CHMOD 755 foo', 'QUOTA') using SITE. `callback` has 3 parameters: < _Error_ >err, < _string >responseText, < _integer_ >responseCode. * **status**(< _function_ >callback) - _(void)_ - Retrieves human-readable information about the server's status. `callback` has 2 parameters: < _Error_ >err, < _string_ >status. * **ascii**(< _function_ >callback) - _(void)_ - Sets the transfer data type to ASCII. `callback` has 1 parameter: < _Error_ >err. * **binary**(< _function_ >callback) - _(void)_ - Sets the transfer data type to binary (default at time of connection). `callback` has 1 parameter: < _Error_ >err. ### Optional "standard" commands (RFC 959) * **mkdir**(< _string_ >path, [< _boolean_ >recursive, ]< _function_ >callback) - _(void)_ - Creates a new directory, `path`, on the server. `recursive` is for enabling a 'mkdir -p' algorithm and defaults to false. `callback` has 1 parameter: < _Error_ >err. * **rmdir**(< _string_ >path, [< _boolean_ >recursive, ]< _function_ >callback) - _(void)_ - Removes a directory, `path`, on the server. If `recursive`, this call will delete the contents of the directory if it is not empty. `callback` has 1 parameter: < _Error_ >err. * **cdup**(< _function_ >callback) - _(void)_ - Changes the working directory to the parent of the current directory. `callback` has 1 parameter: < _Error_ >err. * **pwd**(< _function_ >callback) - _(void)_ - Retrieves the current working directory. `callback` has 2 parameters: < _Error_ >err, < _string_ >cwd. * **system**(< _function_ >callback) - _(void)_ - Retrieves the server's operating system. `callback` has 2 parameters: < _Error_ >err, < _string_ >OS. * **listSafe**([< _string_ >path, ][< _boolean_ >useCompression, ]< _function_ >callback) - _(void)_ - Similar to list(), except the directory is temporarily changed to `path` to retrieve the directory listing. This is useful for servers that do not handle characters like spaces and quotes in directory names well for the LIST command. This function is "optional" because it relies on pwd() being available. ### Extended commands (RFC 3659) * **size**(< _string_ >path, < _function_ >callback) - _(void)_ - Retrieves the size of `path`. `callback` has 2 parameters: < _Error_ >err, < _integer_ >numBytes. * **lastMod**(< _string_ >path, < _function_ >callback) - _(void)_ - Retrieves the last modified date and time for `path`. `callback` has 2 parameters: < _Error_ >err, < _Date_ >lastModified. * **restart**(< _integer_ >byteOffset, < _function_ >callback) - _(void)_ - Sets the file byte offset for the next file transfer action (get/put) to `byteOffset`. `callback` has 1 parameter: < _Error_ >err. ================================================ FILE: app/back-end/modules/custom-changes/ftp/TODO ================================================ - Add support for some SITE commands such as CHMOD, CHGRP, and QUOTA - Active (non-passive) data connections - IPv6 support ================================================ FILE: app/back-end/modules/custom-changes/ftp/lib/connection.js ================================================ var fs = require('fs'), tls = require('tls'), zlib = require('zlib'), Socket = require('net').Socket, EventEmitter = require('events').EventEmitter, inherits = require('util').inherits, inspect = require('util').inspect; var Parser = require('./parser'); var XRegExp = require('xregexp').XRegExp; var REX_TIMEVAL = XRegExp.cache('^(?\\d{4})(?\\d{2})(?\\d{2})(?\\d{2})(?\\d{2})(?\\d+)(?:.\\d+)?$'), RE_PASV = /([\d]+),([\d]+),([\d]+),([\d]+),([-\d]+),([-\d]+)/, RE_EOL = /\r?\n/g, RE_WD = /"(.+)"(?: |$)/, RE_SYST = /^([^ ]+)(?: |$)/; var /*TYPE = { SYNTAX: 0, INFO: 1, SOCKETS: 2, AUTH: 3, UNSPEC: 4, FILESYS: 5 },*/ RETVAL = { PRELIM: 1, OK: 2, WAITING: 3, ERR_TEMP: 4, ERR_PERM: 5 }, /*ERRORS = { 421: 'Service not available, closing control connection', 425: 'Can\'t open data connection', 426: 'Connection closed; transfer aborted', 450: 'Requested file action not taken / File unavailable (e.g., file busy)', 451: 'Requested action aborted: local error in processing', 452: 'Requested action not taken / Insufficient storage space in system', 500: 'Syntax error / Command unrecognized', 501: 'Syntax error in parameters or arguments', 502: 'Command not implemented', 503: 'Bad sequence of commands', 504: 'Command not implemented for that parameter', 530: 'Not logged in', 532: 'Need account for storing files', 550: 'Requested action not taken / File unavailable (e.g., file not found, no access)', 551: 'Requested action aborted: page type unknown', 552: 'Requested file action aborted / Exceeded storage allocation (for current directory or dataset)', 553: 'Requested action not taken / File name not allowed' },*/ bytesNOOP = Buffer.from('NOOP\r\n'); var FTP = module.exports = function() { if (!(this instanceof FTP)) return new FTP(); this._socket = undefined; this._pasvSock = undefined; this._feat = undefined; this._curReq = undefined; this._queue = []; this._secstate = undefined; this._debug = undefined; this._keepalive = undefined; this._ending = false; this._parser = undefined; this.options = { host: undefined, port: undefined, user: undefined, password: undefined, secure: false, secureOptions: undefined, connTimeout: undefined, pasvTimeout: undefined, aliveTimeout: undefined }; this.connected = false; }; inherits(FTP, EventEmitter); FTP.prototype.connect = function(options) { var self = this; if (typeof options !== 'object') options = {}; this.connected = false; this.options.host = options.host || 'localhost'; this.options.port = options.port || 21; this.options.user = options.user || 'anonymous'; this.options.password = options.password || 'anonymous@'; this.options.secure = options.secure || false; this.options.secureOptions = options.secureOptions; this.options.connTimeout = options.connTimeout || 10000; this.options.pasvTimeout = options.pasvTimeout || 10000; this.options.aliveTimeout = options.keepalive || 10000; if (typeof options.debug === 'function') this._debug = options.debug; var secureOptions, debug = this._debug, socket = new Socket(); socket.setTimeout(0); socket.setKeepAlive(false); this._parser = new Parser({ debug: debug }); this._parser.on('response', function(code, text) { var retval = code / 100 >> 0; if (retval === RETVAL.ERR_TEMP || retval === RETVAL.ERR_PERM) { if (self._curReq) self._curReq.cb(makeError(code, text), undefined, code); else self.emit('error', makeError(code, text)); } else if (self._curReq) self._curReq.cb(undefined, text, code); // a hack to signal we're waiting for a PASV data connection to complete // first before executing any more queued requests ... // // also: don't forget our current request if we're expecting another // terminating response .... if (self._curReq && retval !== RETVAL.PRELIM) { self._curReq = undefined; self._send(); } noopreq.cb(); }); if (this.options.secure) { secureOptions = {}; secureOptions.host = this.options.host; for (var k in this.options.secureOptions) secureOptions[k] = this.options.secureOptions[k]; secureOptions.socket = socket; this.options.secureOptions = secureOptions; } if (this.options.secure === 'implicit') this._socket = tls.connect(secureOptions, onconnect); else { socket.once('connect', onconnect); this._socket = socket; } var noopreq = { cmd: 'NOOP', cb: function() { clearTimeout(self._keepalive); self._keepalive = setTimeout(donoop, self.options.aliveTimeout); } }; function donoop() { if (!self._socket || !self._socket.writable) clearTimeout(self._keepalive); else if (!self._curReq && self._queue.length === 0) { self._curReq = noopreq; debug&&debug('[connection] > NOOP'); self._socket.write(bytesNOOP); } else noopreq.cb(); } function onconnect() { clearTimeout(timer); clearTimeout(self._keepalive); self.connected = true; self._socket = socket; // re-assign for implicit secure connections var cmd; if (self._secstate) { if (self._secstate === 'upgraded-tls' && self.options.secure === true) { cmd = 'PBSZ'; self._send('PBSZ 0', reentry, true); } else { cmd = 'USER'; self._send('USER ' + self.options.user, reentry, true); } } else { self._curReq = { cmd: '', cb: reentry }; } function reentry(err, text, code) { if (err && (!cmd || cmd === 'USER' || cmd === 'PASS' || cmd === 'TYPE')) { self.emit('error', err); return self._socket && self._socket.end(); } if ((cmd === 'AUTH TLS' && code !== 234 && self.options.secure !== true) || (cmd === 'AUTH SSL' && code !== 334) || (cmd === 'PBSZ' && code !== 200) || (cmd === 'PROT' && code !== 200)) { self.emit('error', makeError(code, 'Unable to secure connection(s)')); return self._socket && self._socket.end(); } if (!cmd) { // sometimes the initial greeting can contain useful information // about authorized use, other limits, etc. self.emit('greeting', text); if (self.options.secure && self.options.secure !== 'implicit') { cmd = 'AUTH TLS'; self._send(cmd, reentry, true); } else { cmd = 'USER'; self._send('USER ' + self.options.user, reentry, true); } } else if (cmd === 'USER') { if (code !== 230) { // password required if (!self.options.password) { self.emit('error', makeError(code, 'Password required')); return self._socket && self._socket.end(); } cmd = 'PASS'; self._send('PASS ' + self.options.password, reentry, true); } else { // no password required cmd = 'PASS'; reentry(undefined, text, code); } } else if (cmd === 'PASS') { cmd = 'FEAT'; self._send(cmd, reentry, true); } else if (cmd === 'FEAT') { if (!err) self._feat = Parser.parseFeat(text); cmd = 'TYPE'; self._send('TYPE I', reentry, true); } else if (cmd === 'TYPE') self.emit('ready'); else if (cmd === 'PBSZ') { cmd = 'PROT'; self._send('PROT P', reentry, true); } else if (cmd === 'PROT') { cmd = 'USER'; self._send('USER ' + self.options.user, reentry, true); } else if (cmd.substr(0, 4) === 'AUTH') { if (cmd === 'AUTH TLS' && code !== 234) { cmd = 'AUTH SSL'; return self._send(cmd, reentry, true); } else if (cmd === 'AUTH TLS') self._secstate = 'upgraded-tls'; else if (cmd === 'AUTH SSL') self._secstate = 'upgraded-ssl'; socket.removeAllListeners('data'); socket.removeAllListeners('error'); socket._decoder = null; self._curReq = null; // prevent queue from being processed during // TLS/SSL negotiation secureOptions.socket = self._socket; secureOptions.session = undefined; socket = tls.connect(secureOptions, onconnect); socket.setEncoding('binary'); socket.on('data', ondata); socket.once('end', onend); socket.on('error', onerror); } } } socket.on('data', ondata); function ondata(chunk) { debug&&debug('[connection] < ' + inspect(chunk.toString('binary'))); if (self._parser) self._parser.write(chunk); } socket.on('error', onerror); function onerror(err) { clearTimeout(timer); clearTimeout(self._keepalive); self.emit('error', err); } socket.once('end', onend); function onend() { ondone(); self.emit('end'); } socket.once('close', function(had_err) { ondone(); self.emit('close', had_err); }); var hasReset = false; function ondone() { if (!hasReset) { hasReset = true; clearTimeout(timer); self._reset(); } } var timer = setTimeout(function() { self.emit('error', new Error('Timeout while connecting to server')); self._socket && self._socket.destroy(); self._reset(); }, this.options.connTimeout); this._socket.connect(this.options.port, this.options.host); }; FTP.prototype.end = function() { if (this._queue.length) this._ending = true; else this._reset(); }; FTP.prototype.destroy = function() { this._reset(); }; // "Standard" (RFC 959) commands FTP.prototype.ascii = function(cb) { return this._send('TYPE A', cb); }; FTP.prototype.binary = function(cb) { return this._send('TYPE I', cb); }; FTP.prototype.abort = function(immediate, cb) { if (typeof immediate === 'function') { cb = immediate; immediate = true; } if (immediate) this._send('ABOR', cb, true); else this._send('ABOR', cb); }; FTP.prototype.cwd = function(path, cb, promote) { this._send('CWD ' + path, function(err, text, code) { if (err) return cb(err); var m = RE_WD.exec(text); cb(undefined, m ? m[1] : undefined); }, promote); }; FTP.prototype.delete = function(path, cb) { this._send('DELE ' + path, cb); }; FTP.prototype.site = function(cmd, cb) { this._send('SITE ' + cmd, cb); }; FTP.prototype.status = function(cb) { this._send('STAT', cb); }; FTP.prototype.rename = function(from, to, cb) { var self = this; this._send('RNFR ' + from, function(err) { if (err) return cb(err); self._send('RNTO ' + to, cb, true); }); }; FTP.prototype.logout = function(cb) { this._send('QUIT', cb); }; FTP.prototype.listSafe = function(path, zcomp, cb) { if (typeof path === 'string') { var self = this; // store current path this.pwd(function(err, origpath) { if (err) return cb(err); // change to destination path self.cwd(path, function(err) { if (err) return cb(err); // get dir listing self.list(zcomp || false, function(err, list) { // change back to original path if (err) return self.cwd(origpath, cb); self.cwd(origpath, function(err) { if (err) return cb(err); cb(err, list); }); }); }); }); } else this.list(path, zcomp, cb); }; FTP.prototype.list = function(path, zcomp, cb) { var self = this, cmd; if (typeof path === 'function') { // list(function() {}) cb = path; path = undefined; cmd = 'LIST'; zcomp = false; } else if (typeof path === 'boolean') { // list(true, function() {}) cb = zcomp; zcomp = path; path = undefined; cmd = 'LIST'; } else if (typeof zcomp === 'function') { // list('/foo', function() {}) cb = zcomp; cmd = 'LIST ' + path; zcomp = false; } else cmd = 'LIST ' + path; this._pasv(function(err, sock) { if (err) return cb(err); if (self._queue[0] && self._queue[0].cmd === 'ABOR') { sock.destroy(); return cb(); } var sockerr, done = false, replies = 0, entries, buffer = '', source = sock; if (zcomp) { source = zlib.createInflate(); sock.pipe(source); } source.on('data', function(chunk) { buffer += chunk.toString('binary'); }); source.once('error', function(err) { if (!sock.aborting) sockerr = err; }); source.once('end', ondone); source.once('close', ondone); function ondone() { done = true; final(); } function final() { if (done && replies === 2) { replies = 3; if (sockerr) return cb(new Error('Unexpected data connection error: ' + sockerr)); if (sock.aborting) return cb(); // process received data entries = buffer.split(RE_EOL); entries.pop(); // ending EOL var parsed = []; for (var i = 0, len = entries.length; i < len; ++i) { var parsedVal = Parser.parseListEntry(entries[i]); if (parsedVal !== null) parsed.push(parsedVal); } if (zcomp) { self._send('MODE S', function() { cb(undefined, parsed); }, true); } else cb(undefined, parsed); } } if (zcomp) { self._send('MODE Z', function(err, text, code) { if (err) { sock.destroy(); return cb(makeError(code, 'Compression not supported')); } sendList(); }, true); } else sendList(); function sendList() { // this callback will be executed multiple times, the first is when server // replies with 150 and then a final reply to indicate whether the // transfer was actually a success or not self._send(cmd, function(err, text, code) { if (err) { sock.destroy(); if (zcomp) { self._send('MODE S', function() { cb(err); }, true); } else cb(err); return; } // some servers may not open a data connection for empty directories if (++replies === 1 && code === 226) { replies = 2; sock.destroy(); final(); } else if (replies === 2) final(); }, true); } }); }; FTP.prototype.get = function(path, zcomp, cb) { var self = this; if (typeof zcomp === 'function') { cb = zcomp; zcomp = false; } this._pasv(function(err, sock) { if (err) return cb(err); if (self._queue[0] && self._queue[0].cmd === 'ABOR') { sock.destroy(); return cb(); } // modify behavior of socket events so that we can emit 'error' once for // either a TCP-level error OR an FTP-level error response that we get when // the socket is closed (e.g. the server ran out of space). var sockerr, started = false, lastreply = false, done = false, source = sock; if (zcomp) { source = zlib.createInflate(); sock.pipe(source); sock._emit = sock.emit; sock.emit = function(ev, arg1) { if (ev === 'error') { if (!sockerr) sockerr = arg1; return; } sock._emit.apply(sock, Array.prototype.slice.call(arguments)); }; } source._emit = source.emit; source.emit = function(ev, arg1) { if (ev === 'error') { if (!sockerr) sockerr = arg1; return; } else if (ev === 'end' || ev === 'close') { if (!done) { done = true; ondone(); } return; } source._emit.apply(source, Array.prototype.slice.call(arguments)); }; function ondone() { if (done && lastreply) { self._send('MODE S', function() { source._emit('end'); source._emit('close'); }, true); } } sock.pause(); if (zcomp) { self._send('MODE Z', function(err, text, code) { if (err) { sock.destroy(); return cb(makeError(code, 'Compression not supported')); } sendRetr(); }, true); } else sendRetr(); function sendRetr() { // this callback will be executed multiple times, the first is when server // replies with 150, then a final reply after the data connection closes // to indicate whether the transfer was actually a success or not self._send('RETR ' + path, function(err, text, code) { if (sockerr || err) { sock.destroy(); if (!started) { if (zcomp) { self._send('MODE S', function() { cb(sockerr || err); }, true); } else cb(sockerr || err); } else { source._emit('error', sockerr || err); source._emit('close', true); } return; } // server returns 125 when data connection is already open; we treat it // just like a 150 if (code === 150 || code === 125) { started = true; cb(undefined, source); sock.resume(); } else { lastreply = true; ondone(); } }, true); } }); }; FTP.prototype.put = function(input, path, zcomp, cb) { this._store('STOR ' + path, input, zcomp, cb); }; FTP.prototype.append = function(input, path, zcomp, cb) { this._store('APPE ' + path, input, zcomp, cb); }; FTP.prototype.pwd = function(cb) { // PWD is optional var self = this; this._send('PWD', function(err, text, code) { if (code === 502) { return self.cwd('.', function(cwderr, cwd) { if (cwderr) return cb(cwderr); if (cwd === undefined) cb(err); else cb(undefined, cwd); }, true); } else if (err) return cb(err); cb(undefined, RE_WD.exec(text)[1]); }); }; FTP.prototype.cdup = function(cb) { // CDUP is optional var self = this; this._send('CDUP', function(err, text, code) { if (code === 502) self.cwd('..', cb, true); else cb(err); }); }; FTP.prototype.mkdir = function(path, recursive, cb) { // MKD is optional if (typeof recursive === 'function') { cb = recursive; recursive = false; } if (!recursive) this._send('MKD ' + path, cb); else { var self = this, owd, abs, dirs, dirslen, i = -1, searching = true; abs = (path[0] === '/'); var nextDir = function() { if (++i === dirslen) { // return to original working directory return self._send('CWD ' + owd, cb, true); } if (searching) { self._send('CWD ' + dirs[i], function(err, text, code) { if (code === 550) { searching = false; --i; } else if (err) { // return to original working directory return self._send('CWD ' + owd, function() { cb(err); }, true); } nextDir(); }, true); } else { self._send('MKD ' + dirs[i], function(err, text, code) { if (err) { // return to original working directory return self._send('CWD ' + owd, function() { cb(err); }, true); } self._send('CWD ' + dirs[i], nextDir, true); }, true); } }; this.pwd(function(err, cwd) { if (err) return cb(err); owd = cwd; if (abs) path = path.substr(1); if (path[path.length - 1] === '/') path = path.substring(0, path.length - 1); dirs = path.split('/'); dirslen = dirs.length; if (abs) self._send('CWD /', function(err) { if (err) return cb(err); nextDir(); }, true); else nextDir(); }); } }; FTP.prototype.rmdir = function(path, recursive, cb) { // RMD is optional if (typeof recursive === 'function') { cb = recursive; recursive = false; } if (!recursive) { return this._send('RMD ' + path, cb); } var self = this; this.list(path, function(err, list) { if (err) return cb(err); var idx = 0; // this function will be called once per listing entry var deleteNextEntry; deleteNextEntry = function(err) { if (err) return cb(err); if (idx >= list.length) { if (list[0] && list[0].name === path) { return cb(null); } else { return self.rmdir(path, cb); } } var entry = list[idx++]; // get the path to the file var subpath = null; if (entry.name[0] === '/') { // this will be the case when you call deleteRecursively() and pass // the path to a plain file subpath = entry.name; } else { if (path[path.length - 1] == '/') { subpath = path + entry.name; } else { subpath = path + '/' + entry.name } } // delete the entry (recursively) according to its type if (entry.type === 'd') { if (entry.name === "." || entry.name === "..") { return deleteNextEntry(); } self.rmdir(subpath, true, deleteNextEntry); } else { self.delete(subpath, deleteNextEntry); } } deleteNextEntry(); }); }; FTP.prototype.system = function(cb) { // SYST is optional this._send('SYST', function(err, text) { if (err) return cb(err); cb(undefined, RE_SYST.exec(text)[1]); }); }; // "Extended" (RFC 3659) commands FTP.prototype.size = function(path, cb) { var self = this; this._send('SIZE ' + path, function(err, text, code) { if (code === 502) { // Note: this may cause a problem as list() is _appended_ to the queue return self.list(path, function(err, list) { if (err) return cb(err); if (list.length === 1) cb(undefined, list[0].size); else { // path could have been a directory and we got a listing of its // contents, but here we echo the behavior of the real SIZE and // return 'File not found' for directories cb(new Error('File not found')); } }, true); } else if (err) return cb(err); cb(undefined, parseInt(text, 10)); }); }; FTP.prototype.lastMod = function(path, cb) { var self = this; this._send('MDTM ' + path, function(err, text, code) { if (code === 502) { return self.list(path, function(err, list) { if (err) return cb(err); if (list.length === 1) cb(undefined, list[0].date); else cb(new Error('File not found')); }, true); } else if (err) return cb(err); var val = XRegExp.exec(text, REX_TIMEVAL), ret; if (!val) return cb(new Error('Invalid date/time format from server')); ret = new Date(val.year + '-' + val.month + '-' + val.date + 'T' + val.hour + ':' + val.minute + ':' + val.second); cb(undefined, ret); }); }; FTP.prototype.restart = function(offset, cb) { this._send('REST ' + offset, cb); }; // Private/Internal methods FTP.prototype._pasv = function(cb) { var self = this, first = true, ip, port; this._send('PASV', function reentry(err, text) { if (err) return cb(err); self._curReq = undefined; if (first) { var m = RE_PASV.exec(text); if (!m) return cb(new Error('Unable to parse PASV server response')); ip = m[1]; ip += '.'; ip += m[2]; ip += '.'; ip += m[3]; ip += '.'; ip += m[4]; port = (parseInt(m[5], 10) * 256) + parseInt(m[6], 10); first = false; } self._pasvConnect(ip, port, function(err, sock) { if (err) { // try the IP of the control connection if the server was somehow // misconfigured and gave for example a LAN IP instead of WAN IP over // the Internet if (self._socket && ip !== self._socket.remoteAddress) { ip = self._socket.remoteAddress; return reentry(); } // automatically abort PASV mode self._send('ABOR', function() { cb(err); self._send(); }, true); return; } cb(undefined, sock); self._send(); }); }); }; FTP.prototype._pasvConnect = function(ip, port, cb) { var self = this, socket = new Socket(), sockerr, timedOut = false, timer = setTimeout(function() { timedOut = true; socket.destroy(); cb(new Error('Timed out while making data connection')); }, this.options.pasvTimeout); socket.setTimeout(0); socket.once('connect', function() { self._debug&&self._debug('[connection] PASV socket connected'); if (self.options.secure === true) { self.options.secureOptions.socket = socket; self.options.secureOptions.session = self._socket.getSession(); //socket.removeAllListeners('error'); socket = tls.connect(self.options.secureOptions); //socket.once('error', onerror); socket.setTimeout(0); } clearTimeout(timer); self._pasvSocket = socket; cb(undefined, socket); }); socket.once('error', onerror); function onerror(err) { sockerr = err; } socket.once('end', function() { clearTimeout(timer); }); socket.once('close', function(had_err) { clearTimeout(timer); if (!self._pasvSocket && !timedOut) { var errmsg = 'Unable to make data connection'; if (sockerr) { errmsg += '( ' + sockerr + ')'; sockerr = undefined; } cb(new Error(errmsg)); } self._pasvSocket = undefined; }); socket.connect(port, ip); }; FTP.prototype._store = function(cmd, input, zcomp, cb) { var isBuffer = Buffer.isBuffer(input); if (!isBuffer && input.pause !== undefined) input.pause(); if (typeof zcomp === 'function') { cb = zcomp; zcomp = false; } var self = this; this._pasv(function(err, sock) { if (err) return cb(err); if (self._queue[0] && self._queue[0].cmd === 'ABOR') { sock.destroy(); return cb(); } var sockerr, dest = sock; sock.once('error', function(err) { sockerr = err; }); if (zcomp) { self._send('MODE Z', function(err, text, code) { if (err) { sock.destroy(); return cb(makeError(code, 'Compression not supported')); } // draft-preston-ftpext-deflate-04 says min of 8 should be supported dest = zlib.createDeflate({ level: 8 }); dest.pipe(sock); sendStore(); }, true); } else sendStore(); function sendStore() { // this callback will be executed multiple times, the first is when server // replies with 150, then a final reply after the data connection closes // to indicate whether the transfer was actually a success or not self._send(cmd, function(err, text, code) { if (sockerr || err) { if (zcomp) { self._send('MODE S', function() { cb(sockerr || err); }, true); } else cb(sockerr || err); return; } if (code === 150 || code === 125) { if (isBuffer) dest.end(input); else if (typeof input === 'string') { // check if input is a file path or just string data to store fs.stat(input, function(err, stats) { if (err) dest.end(input); else fs.createReadStream(input).pipe(dest); }); } else { input.pipe(dest); input.resume(); } } else { if (zcomp) self._send('MODE S', cb, true); else cb(); } }, true); } }); }; FTP.prototype._send = function(cmd, cb, promote) { clearTimeout(this._keepalive); if (cmd !== undefined) { if (promote) this._queue.unshift({ cmd: cmd, cb: cb }); else this._queue.push({ cmd: cmd, cb: cb }); } var queueLen = this._queue.length; if (!this._curReq && queueLen && this._socket && this._socket.readable) { this._curReq = this._queue.shift(); if (this._curReq.cmd === 'ABOR' && this._pasvSocket) this._pasvSocket.aborting = true; this._debug&&this._debug('[connection] > ' + inspect(this._curReq.cmd)); this._socket.write(this._curReq.cmd + '\r\n'); } else if (!this._curReq && !queueLen && this._ending) this._reset(); }; FTP.prototype._reset = function() { if (this._pasvSock && this._pasvSock.writable) this._pasvSock.end(); if (this._socket && this._socket.writable) this._socket.end(); this._socket = undefined; this._pasvSock = undefined; this._feat = undefined; this._curReq = undefined; this._secstate = undefined; clearTimeout(this._keepalive); this._keepalive = undefined; this._queue = []; this._ending = false; this._parser = undefined; this.options.host = this.options.port = this.options.user = this.options.password = this.options.secure = this.options.connTimeout = this.options.pasvTimeout = this.options.keepalive = this._debug = undefined; this.connected = false; }; // Utility functions function makeError(code, text) { var err = new Error(text); err.code = code; return err; } ================================================ FILE: app/back-end/modules/custom-changes/ftp/lib/parser.js ================================================ var WritableStream = require('stream').Writable || require('readable-stream').Writable, inherits = require('util').inherits, inspect = require('util').inspect; var XRegExp = require('xregexp').XRegExp; var REX_LISTUNIX = XRegExp.cache('^(?[\\-ld])(?([\\-r][\\-w][\\-xstT]){3})(?(\\+))?\\s+(?\\d+)\\s+(?\\S+)\\s+(?\\S+)\\s+(?\\d+)\\s+(?((?\\w{3})\\s+(?\\d{1,2})\\s+(?\\d{1,2}):(?\\d{2}))|((?\\w{3})\\s+(?\\d{1,2})\\s+(?\\d{4})))\\s+(?.+)$'), REX_LISTMSDOS = XRegExp.cache('^(?\\d{2})(?:\\-|\\/)(?\\d{2})(?:\\-|\\/)(?\\d{2,4})\\s+(?\\d{2}):(?\\d{2})\\s{0,1}(?[AaMmPp]{1,2})\\s+(?:(?\\d+)|(?\\))\\s+(?.+)$'), RE_ENTRY_TOTAL = /^total/, RE_RES_END = /(?:^|\r?\n)(\d{3}) [^\r\n]*\r?\n/, RE_EOL = /\r?\n/g, RE_DASH = /\-/g; var MONTHS = { jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6, jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12 }; function Parser(options) { if (!(this instanceof Parser)) return new Parser(options); WritableStream.call(this); this._buffer = ''; this._debug = options.debug; } inherits(Parser, WritableStream); Parser.prototype._write = function(chunk, encoding, cb) { var m, code, reRmLeadCode, rest = '', debug = this._debug; this._buffer += chunk.toString('binary'); while (m = RE_RES_END.exec(this._buffer)) { // support multiple terminating responses in the buffer rest = this._buffer.substring(m.index + m[0].length); if (rest.length) this._buffer = this._buffer.substring(0, m.index + m[0].length); debug&&debug('[parser] < ' + inspect(this._buffer)); // we have a terminating response line code = parseInt(m[1], 10); // RFC 959 does not require each line in a multi-line response to begin // with '-', but many servers will do this. // // remove this leading '-' (or ' ' from last line) from each // line in the response ... reRmLeadCode = '(^|\\r?\\n)'; reRmLeadCode += m[1]; reRmLeadCode += '(?: |\\-)'; reRmLeadCode = new RegExp(reRmLeadCode, 'g'); var text = this._buffer.replace(reRmLeadCode, '$1').trim(); this._buffer = rest; debug&&debug('[parser] Response: code=' + code + ', buffer=' + inspect(text)); this.emit('response', code, text); } cb(); }; Parser.parseFeat = function(text) { var lines = text.split(RE_EOL); lines.shift(); // initial response line lines.pop(); // final response line for (var i = 0, len = lines.length; i < len; ++i) lines[i] = lines[i].trim(); // just return the raw lines for now return lines; }; Parser.parseListEntry = function(line) { var ret, info, month, day, year, hour, mins; if (ret = XRegExp.exec(line, REX_LISTUNIX)) { info = { type: ret.type, name: undefined, target: undefined, sticky: false, rights: { user: ret.permission.substr(0, 3).replace(RE_DASH, ''), group: ret.permission.substr(3, 3).replace(RE_DASH, ''), other: ret.permission.substr(6, 3).replace(RE_DASH, '') }, acl: (ret.acl === '+'), owner: ret.owner, group: ret.group, size: parseInt(ret.size, 10), date: undefined }; // check for sticky bit var lastbit = info.rights.other.slice(-1); if (lastbit === 't') { info.rights.other = info.rights.other.slice(0, -1) + 'x'; info.sticky = true; } else if (lastbit === 'T') { info.rights.other = info.rights.other.slice(0, -1); info.sticky = true; } if (ret.month1 !== undefined) { month = parseInt(MONTHS[ret.month1.toLowerCase()], 10); day = parseInt(ret.date1, 10); year = (new Date()).getFullYear(); hour = parseInt(ret.hour, 10); mins = parseInt(ret.minute, 10); if (month < 10) month = '0' + month; if (day < 10) day = '0' + day; if (hour < 10) hour = '0' + hour; if (mins < 10) mins = '0' + mins; info.date = new Date(year + '-' + month + '-' + day + 'T' + hour + ':' + mins); // If the date is in the past but no more than 6 months old, year // isn't displayed and doesn't have to be the current year. // // If the date is in the future (less than an hour from now), year // isn't displayed and doesn't have to be the current year. // That second case is much more rare than the first and less annoying. // It's impossible to fix without knowing about the server's timezone, // so we just don't do anything about it. // // If we're here with a time that is more than 28 hours into the // future (1 hour + maximum timezone offset which is 27 hours), // there is a problem -- we should be in the second conditional block if (info.date.getTime() - Date.now() > 100800000) { info.date = new Date((year - 1) + '-' + month + '-' + day + 'T' + hour + ':' + mins); } // If we're here with a time that is more than 6 months old, there's // a problem as well. // Maybe local & remote servers aren't on the same timezone (with remote // ahead of local) // For instance, remote is in 2014 while local is still in 2013. In // this case, a date like 01/01/13 02:23 could be detected instead of // 01/01/14 02:23 // Our trigger point will be 3600*24*31*6 (since we already use 31 // as an upper bound, no need to add the 27 hours timezone offset) if (Date.now() - info.date.getTime() > 16070400000) { info.date = new Date((year + 1) + '-' + month + '-' + day + 'T' + hour + ':' + mins); } } else if (ret.month2 !== undefined) { month = parseInt(MONTHS[ret.month2.toLowerCase()], 10); day = parseInt(ret.date2, 10); year = parseInt(ret.year, 10); if (month < 10) month = '0' + month; if (day < 10) day = '0' + day; info.date = new Date(year + '-' + month + '-' + day); } if (ret.type === 'l') { var pos = ret.name.indexOf(' -> '); info.name = ret.name.substring(0, pos); info.target = ret.name.substring(pos+4); } else info.name = ret.name; ret = info; } else if (ret = XRegExp.exec(line, REX_LISTMSDOS)) { info = { name: ret.name, type: (ret.isdir ? 'd' : '-'), size: (ret.isdir ? 0 : parseInt(ret.size, 10)), date: undefined, }; month = parseInt(ret.month, 10), day = parseInt(ret.date, 10), year = parseInt(ret.year, 10), hour = parseInt(ret.hour, 10), mins = parseInt(ret.minute, 10); if (year < 70) year += 2000; else year += 1900; if (ret.ampm[0].toLowerCase() === 'p' && hour < 12) hour += 12; else if (ret.ampm[0].toLowerCase() === 'a' && hour === 12) hour = 0; info.date = new Date(year, month - 1, day, hour, mins); ret = info; } else if (!RE_ENTRY_TOTAL.test(line)) ret = line; // could not parse, so at least give the end user a chance to // look at the raw listing themselves return ret; }; module.exports = Parser; ================================================ FILE: app/back-end/modules/custom-changes/ftp/package.json ================================================ { "_args": [ [ { "raw": "ftp@^0.3.10", "scope": null, "escapedName": "ftp", "name": "ftp", "rawSpec": "^0.3.10", "spec": ">=0.3.10 <0.4.0", "type": "range" }, "/Users/dziudek/Desktop/Publii/app" ] ], "_from": "ftp@>=0.3.10 <0.4.0", "_id": "ftp@0.3.10", "_inCache": true, "_installable": true, "_location": "/ftp", "_npmUser": { "name": "mscdex", "email": "mscdex@mscdex.net" }, "_npmVersion": "1.4.28", "_phantomChildren": {}, "_requested": { "raw": "ftp@^0.3.10", "scope": null, "escapedName": "ftp", "name": "ftp", "rawSpec": "^0.3.10", "spec": ">=0.3.10 <0.4.0", "type": "range" }, "_requiredBy": [ "/" ], "_resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", "_shasum": "9197d861ad8142f3e63d5a83bfe4c59f7330885d", "_shrinkwrap": null, "_spec": "ftp@^0.3.10", "_where": "/Users/dziudek/Desktop/Publii/app", "author": { "name": "Brian White", "email": "mscdex@mscdex.net" }, "bugs": { "url": "https://github.com/mscdex/node-ftp/issues" }, "dependencies": { "readable-stream": "1.1.x", "xregexp": "2.0.0" }, "description": "An FTP client module for node.js", "devDependencies": {}, "directories": {}, "dist": { "shasum": "9197d861ad8142f3e63d5a83bfe4c59f7330885d", "tarball": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz" }, "engines": { "node": ">=0.8.0" }, "homepage": "https://github.com/mscdex/node-ftp", "keywords": [ "ftp", "client", "transfer" ], "licenses": [ { "type": "MIT", "url": "http://github.com/mscdex/node-ftp/raw/master/LICENSE" } ], "main": "./lib/connection", "maintainers": [ { "name": "mscdex", "email": "mscdex@mscdex.net" } ], "name": "ftp", "optionalDependencies": {}, "readme": "ERROR: No README data found!", "repository": { "type": "git", "url": "git+ssh://git@github.com/mscdex/node-ftp.git" }, "scripts": { "test": "node test/test.js" }, "version": "0.3.10" } ================================================ FILE: app/back-end/modules/deploy/deployment.js ================================================ // Necessary packages const fs = require('fs-extra'); const path = require('path'); const FileHelper = require('./../../helpers/file.js'); const crypto = require('crypto'); const normalizePath = require('normalize-path'); const isBinaryFileSync = require('isbinaryfile').isBinaryFileSync; const slug = require('./../../helpers/slug'); const FTP = require('./ftp.js'); const FTPAlt = require('./ftp-alt.js'); const SFTP = require('./sftp.js'); const S3 = require('./s3.js'); const Git = require('./git.js'); const GithubPages = require('./github-pages.js'); const GitlabPages = require('./gitlab-pages.js'); const Netlify = require('./netlify.js'); const GoogleCloud = require('./google-cloud.js'); const ManualDeployment = require('./manual.js'); /** * * Class used to upload files to: * * (S)FTP(S), * S3 server, * Git * Github Pages, * Gitlab Pages, * Netlify, * Google Cloud, * Manually * */ class Deployment { /** * Constructor * * @param appDir * @param sitesDir * @param siteConfig */ constructor (appDir, sitesDir, siteConfig, useAltFtp) { this.appDir = appDir; this.siteConfig = siteConfig; this.siteName = this.siteConfig.name; this.sitesDir = sitesDir; this.useAltFtp = useAltFtp; this.progressOfDeleting = 0; this.progressOfUploading = 0; this.client = false; this.filesToRemove = []; this.filesToUpload = []; this.operationsCounter = 0; this.syncRevision = crypto.randomUUID(); } /** * Tests connection * * @param app * @param deploymentConfig * @param siteName */ async testConnection (app, deploymentConfig, siteName, uuid) { let connection = false; switch(deploymentConfig.protocol) { case 'sftp': case 'sftp+key': connection = new SFTP(); break; case 's3': connection = new S3(); break; case 'netlify': connection = new Netlify(); break; case 'google-cloud': connection = new GoogleCloud(); break; case 'git': connection = new Git(); break; case 'github-pages': connection = new GithubPages(deploymentConfig); break; case 'gitlab-pages': connection = new GitlabPages(); break; default: if (this.useAltFtp) { connection = new FTPAlt(); } else { connection = new FTP(); } break; } if (connection) { await connection.testConnection(app, deploymentConfig, siteName, uuid); } } /** * Inits connection */ async initSession () { switch(this.siteConfig.deployment.protocol) { case 'sftp': case 'sftp+key': this.client = new SFTP(this); break; case 's3': this.client = new S3(this); break; case 'git': this.client = new Git(this); break; case 'github-pages': this.client = new GithubPages(this); break; case 'gitlab-pages': this.client = new GitlabPages(this); break; case 'netlify': this.client = new Netlify(this); break; case 'google-cloud': this.client = new GoogleCloud(this); break; case 'manual': this.client = new ManualDeployment(this); break; default: if (this.useAltFtp) { this.client = new FTPAlt(this); } else { this.client = new FTP(this); } break; } await this.client.initConnection(); } /** * Set input directory on local machine */ setInput () { // Set the output dir as a source of the files to upload let basePath = path.join(this.sitesDir, this.siteName); this.inputDir = path.join(basePath, 'output'); this.configDir = path.join(this.sitesDir, this.siteName, 'input', 'config'); } /** * Sets output directory on the server */ setOutput (useEmpty = false) { if (useEmpty) { this.outputDir = ''; } else { this.outputDir = this.siteConfig.deployment.path; } } /** * Prepares list of local files */ prepareLocalFilesList () { let tempFileList = this.readDirRecursiveSync(this.inputDir); let fileList = []; for (let filePath of tempFileList) { if (filePath === '.git') { continue; } if (filePath === '.htaccess' || filePath === '.htpasswd' || filePath === '_redirects') { let excludedProtocols = ['s3', 'github-pages', 'google-cloud', 'netlify']; if (excludedProtocols.indexOf(this.siteConfig.deployment.protocol) === -1) { fileList.push({ path: filePath, type: 'file', md5: crypto.createHash('md5').update(FileHelper.readFileSync(path.join(this.inputDir, filePath))).digest('hex') }); } continue; } // Put directory if (fs.lstatSync(path.join(this.inputDir, filePath)).isDirectory()) { if (filePath.indexOf('/') === 0) { filePath = filePath.substr(1); } if ( this.siteConfig.deployment.protocol !== 'google-cloud' && this.siteConfig.deployment.protocol !== 'gitlab-pages' ) { fileList.push({ path: filePath, type: 'directory', md5: false }); } continue; } // Put file let fileMD5 = false; if (isBinaryFileSync(path.join(this.inputDir, filePath))) { let stats = fs.statSync(path.join(this.inputDir, filePath)); // below operations are required for backward-compatibility with previously used md5 module // it differently handled integer values let fileSizePrepared = Buffer.from((stats.size).toString().split('')); fileMD5 = crypto.createHash('md5').update(fileSizePrepared).digest('hex'); } else { fileMD5 = crypto.createHash('md5').update(FileHelper.readFileSync(path.join(this.inputDir, filePath))).digest('hex'); } fileList.push({ path: filePath, type: 'file', md5: fileMD5 }); } // Save the files list fs.writeFileSync( path.join(this.inputDir, 'files.publii.json'), JSON.stringify(fileList, null, 4), {'flags': 'w'} ); } /** * Check if local list is equal to the server expected copy * * @param fileContent */ checkLocalListWithRemoteList (fileContent) { try { if (typeof fileContent === 'Buffer') { fileContent = fileContent.toString(); } let content = JSON.parse(fileContent); if (content.revision) { let syncRevisionPath = path.join(this.configDir, 'sync-revision.json'); let revisionID = false; if (fs.existsSync(syncRevisionPath)) { let syncRevisionContent = FileHelper.readFileSync(syncRevisionPath); syncRevisionContent = JSON.parse(syncRevisionContent); revisionID = syncRevisionContent.revision; } if (revisionID) { let isExpectedCopy = revisionID === content.revision; this.compareFilesList(isExpectedCopy); } else { let filesToCheck = FileHelper.readFileSync(path.join(this.configDir, 'files-remote.json')); let checkSum = crypto.createHash('md5').update(filesToCheck).digest('hex'); let isExpectedCopy = checkSum === content.revision; this.compareFilesList(isExpectedCopy); } } else { fs.writeFileSync(path.join(this.configDir, 'files-remote.json'), fileContent); this.compareFilesList(true); } } catch (e) { this.compareFilesList(false); } } /** * Compares remote and local files lists * * @param remoteFileListExists */ compareFilesList (remoteFileListExists = false) { let remoteFiles = false; if (remoteFileListExists) { remoteFiles = FileHelper.readFileSync(path.join(this.configDir, 'files-remote.json'), 'utf8'); if (remoteFiles) { try { remoteFiles = JSON.parse(remoteFiles); if (this.siteConfig.deployment.protocol === 'gitlab-pages') { remoteFiles = remoteFiles.map(file => { return file; }); } } catch (e) { remoteFiles = false; console.log('Malformed files-remote.json file: ' + e); } } } // wait for user interaction if there are no remote files list and syncDate exists under site configuration if (!remoteFiles && this.siteConfig.syncDate) { process.send({ type: 'web-contents', message: 'no-remote-files', value: false }); return; } this.continueSync(remoteFiles); } /** * Wait for user answer or just continue sync if remote files list exists */ continueSync (remoteFiles) { let localFiles = FileHelper.readFileSync(path.join(this.inputDir, 'files.publii.json'), 'utf8'); if (localFiles) { localFiles = JSON.parse(localFiles); } // Detect files to remove let filesToRemove = []; if (remoteFiles) { for (let remoteFile of remoteFiles) { let fileFounded = false; for (let localFile of localFiles) { if (localFile.path === remoteFile.path) { fileFounded = true; break; } } if (!fileFounded) { if ( (this.siteConfig.deployment.protocol === 'google-cloud' || this.siteConfig.deployment.protocol === 'gitlab-pages') && remoteFile.type === 'directory' ) { continue; } filesToRemove.push({ path: remoteFile.path, type: remoteFile.type }); } } } // Detect files to upload let filesToUpload = []; for (let localFile of localFiles) { let fileShouldBeUploaded = true; if (remoteFiles) { for (let remoteFile of remoteFiles) { if( localFile.path === remoteFile.path && localFile.md5 === remoteFile.md5 ) { fileShouldBeUploaded = false; break; } } } if (fileShouldBeUploaded) { if ( ( this.siteConfig.deployment.protocol === 'google-cloud' || this.siteConfig.deployment.protocol === 'gitlab-pages' ) && localFile.type === 'directory' ) { continue; } filesToUpload.push({ path: localFile.path, type: localFile.type }); } } this.filesToRemove = filesToRemove; this.filesToUpload = filesToUpload; if (this.siteConfig.deployment.protocol === 's3') { this.operationsCounter = this.filesToRemove.filter(file => file.type === 'file').length + this.filesToUpload.filter(file => file.type === 'file').length + 1; } else { this.operationsCounter = this.filesToRemove.length + this.filesToUpload.length + 1; } this.currentOperationNumber = 0; console.log('Founded ' + this.operationsCounter + ' operations to do'); this.progressPerFile = 90.0 / this.operationsCounter; this.sortFiles(); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8, operations: false } }); this.removeFile(); } /** * Move files or directories to the beginning */ sortFiles () { this.filesToRemove = this.filesToRemove.sort(function(fileA, fileB) { if(fileA.type === 'directory') { return -1; } if(fileB.type === 'directory') { return 1; } return 0; }); this.filesToUpload = this.filesToUpload.sort((fileA, fileB) => { if (fileA.type === 'directory') { return 1; } if (fileB.type === 'directory') { return -1; } // Images will be uploaded at the end if (isBinaryFileSync(path.join(this.inputDir, fileA.path))) { return -1; } if (isBinaryFileSync(path.join(this.inputDir, fileB.path))) { return 1; } return 0; }); // Reorder directories to put higher order directories at the beginning this.filesToUpload = this.filesToUpload.sort(function(fileA, fileB) { if (fileA.type === 'directory' && fileB.type === 'directory') { if (fileA.path.length <= fileB.path.length) { return 1; } else { return -1; } } if (fileA.type === 'directory') { return 1; } if (fileB.type === 'directory') { return -1; } return 0; }); // Reorder directories only this.saveConnectionFilesLog(JSON.stringify(this.filesToUpload), 'to-upload'); this.saveConnectionFilesLog(JSON.stringify(this.filesToRemove), 'to-delete'); } /** * Removes file */ removeFile () { if (this.siteConfig.deployment.protocol === 's3') { this.client.removeFile(); return; } if (this.siteConfig.deployment.protocol === 'gitlab-pages') { this.client.startSync(); return; } let self = this; if (this.filesToRemove.length > 0) { let fileToRemove = this.filesToRemove.pop(); if (fileToRemove.type === 'file') { this.client.removeFile(normalizePath(path.join(this.outputDir, fileToRemove.path))); } else { this.client.removeDirectory(normalizePath(path.join(this.outputDir, fileToRemove.path))); } } else { self.progressOfUploading = self.progressOfDeleting; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(self.progressOfUploading), operations: [self.currentOperationNumber ,self.operationsCounter] } }); this.uploadFile(); } } /** * Uploads file */ uploadFile () { let self = this; if (this.filesToUpload.length > 0) { let fileToUpload = this.filesToUpload.pop(); if (fileToUpload.type === 'file') { this.client.uploadFile( normalizePath(path.join(this.inputDir, fileToUpload.path)), normalizePath(path.join(this.outputDir, fileToUpload.path)) ); } else { this.client.uploadDirectory( normalizePath(path.join(this.inputDir, fileToUpload.path)), normalizePath(path.join(this.outputDir, fileToUpload.path)) ); } } else { process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 98, operations: [ self.currentOperationNumber, self.operationsCounter ] } }); this.client.uploadNewFileList(); } } /** * Function used to get recursive list of the files and directories * in the specific dir * * @param dir * @param filelist * * @returns {Array} */ readDirRecursiveSync (dir, filelist) { let self = this; let files = fs.readdirSync(dir); filelist = filelist || []; files.forEach(function(file) { if (file === '.git') { return; } if (fs.statSync(path.join(dir, file)).isDirectory()) { filelist.push(normalizePath(path.join(dir.replace(self.inputDir, ''), file))); filelist = self.readDirRecursiveSync(path.join(dir, file), filelist); } else { if(file.indexOf('.') !== 0 || file === '.htaccess' || file === '.htpasswd' || file === '_redirects') { filelist.push(normalizePath(path.join(dir.replace(self.inputDir, ''), file))); } } }); return filelist; }; /** * Save connection files log * * @param files * @param suffix */ saveConnectionFilesLog (files, suffix = '') { if (suffix !== '') { suffix = '-' + suffix; } let logPath = path.join(this.appDir, 'connection-files-log' + suffix + '.txt'); fs.writeFileSync(logPath, files); } } module.exports = Deployment; ================================================ FILE: app/back-end/modules/deploy/ftp-alt.js ================================================ /* * Class used to upload files to the FTP(S) server */ const fs = require('fs-extra'); const path = require('path'); const FileHelper = require('./../../helpers/file.js'); const ftp = require('basic-ftp'); const passwordSafeStorage = require('keytar'); const slug = require('./../../helpers/slug'); const normalizePath = require('normalize-path'); const stripTags = require('striptags'); class FTPAlt { constructor(deploymentInstance = false) { this.deployment = deploymentInstance; this.connection = false; this.softUploadErrors = {}; this.hardUploadErrors = []; } async initConnection() { let waitForTimeout = true; let ftpPassword = this.deployment.siteConfig.deployment.password; let account = slug(this.deployment.siteConfig.name); let secureConnection = false; if (this.deployment.siteConfig.uuid) { account = this.deployment.siteConfig.uuid; } this.connection = new ftp.Client(15000); this.connection.ftp.verbose = true; this.connection.ftp.log = this.connectionDebugger; if (ftpPassword === 'publii ' + account) { ftpPassword = await passwordSafeStorage.getPassword('publii', account); } if (this.deployment.siteConfig.deployment.protocol !== 'ftp') { secureConnection = true; } let connectionParams = { host: this.deployment.siteConfig.deployment.server, port: this.deployment.siteConfig.deployment.port, user: this.deployment.siteConfig.deployment.username, password: ftpPassword, secure: secureConnection, secureOptions: { host: this.deployment.siteConfig.deployment.server, port: this.deployment.siteConfig.deployment.port, user: this.deployment.siteConfig.deployment.username, password: ftpPassword, rejectUnauthorized: this.deployment.siteConfig.deployment.rejectUnauthorized } }; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 6, operations: false } }); process.send({ type: 'web-contents', message: 'app-connection-in-progress' }); await this.connection.access(connectionParams); waitForTimeout = false; process.send({ type: 'web-contents', message: 'app-connection-success' }); this.deployment.setInput(); this.deployment.setOutput(); this.deployment.prepareLocalFilesList(); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 7, operations: false } }); this.downloadFilesList(); setTimeout(function() { if (waitForTimeout === true) { this.connection.close(); console.log(`[${ new Date().toUTCString() }] Request timeout...`); process.send({ type: 'web-contents', message: 'app-connection-error' }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); } }, 20000); } async downloadFilesList() { try { await this.connection.downloadTo( normalizePath(path.join(this.deployment.configDir, 'remote-files.json')), normalizePath(path.join(this.deployment.outputDir, 'files.publii.json')) ); let fileToCompare = FileHelper.readFileSync(normalizePath(path.join(this.deployment.configDir, 'remote-files.json'))); this.deployment.checkLocalListWithRemoteList(fileToCompare); console.log(`[${ new Date().toUTCString() }] <- files.publii.json`); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8, operations: false } }); } catch (err) { console.log(`[${ new Date().toUTCString() }] (!) ERROR WHILE DOWNLOADING files-remote.json`); console.log(`[${ new Date().toUTCString() }] ${err}`); this.deployment.compareFilesList(false); } } async uploadNewFileList() { process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 99, operations: [this.deployment.currentOperationNumber, this.deployment.operationsCounter] } }); try { await this.connection.uploadFrom( normalizePath(path.join(this.deployment.inputDir, 'files.publii.json')), normalizePath(path.join(this.deployment.outputDir, 'files.publii.json')), ); console.log(`[${ new Date().toUTCString() }] -> files.publii.json`); } catch (err) { console.log(`[${ new Date().toUTCString() }] ${err}`); } this.connection.close(); console.log(`[${ new Date().toUTCString() }] FTP CONNECTION CLOSED`); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 100, operations: false } }); process.send({ type: 'sender', message: 'app-deploy-uploaded', value: { status: true, issues: this.hardUploadErrors.length > 0 } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); } async uploadFile(input, output) { try { await this.connection.uploadFrom(input, output) } catch (err) { console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD FILE: ${output}`); console.log(`[${ new Date().toUTCString() }] ${err}`); setTimeout(() => { if (!this.softUploadErrors[input]) { this.softUploadErrors[input] = 1; } else { this.softUploadErrors[input]++; } if (this.softUploadErrors[input] <= 5) { this.uploadFile(input, output); } else { this.hardUploadErrors.push(input); this.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] UPL HARD ERR ${input} -> ${output}`); this.deployment.progressOfUploading += this.deployment.progressPerFile; this.updateProgress('progressOfUploading'); this.deployment.uploadFile(); } }, 500); return; } this.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${output}`); this.deployment.progressOfUploading += this.deployment.progressPerFile; this.updateProgress('progressOfUploading'); this.deployment.uploadFile(); } async uploadDirectory(input, output) { try { await this.connection.ensureDir(output); } catch (err) { console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD DIR: ${output}`); console.log(`[${ new Date().toUTCString() }] ${err}`); setTimeout(async () => { if(!this.softUploadErrors[input]) { this.softUploadErrors[input] = 1; } else { this.softUploadErrors[input]++; } if (this.softUploadErrors[input] <= 5) { await this.uploadDirectory(input, output); } else { this.hardUploadErrors.push(input); this.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] UPL HARD ERR ${input} -> ${output}`); this.deployment.progressOfUploading += this.deployment.progressPerFile; this.updateProgress('progressOfUploading'); this.deployment.uploadFile(); } }, 500); return; } try { let rootPath = this.deployment.outputDir; if (!rootPath) { rootPath = '/'; } await this.connection.cd(rootPath); } catch (err) { console.log(`[${ new Date().toUTCString() }] CD error ${err.message}`); } this.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${output}`); this.deployment.progressOfUploading += this.deployment.progressPerFile; this.updateProgress('progressOfUploading'); this.deployment.uploadFile(); } async removeFile(input) { try { await this.connection.remove(input); console.log(`[${ new Date().toUTCString() }] DEL ${input}`); } catch (err) { console.log(`[${ new Date().toUTCString() }] ERROR REMOVE FILE: ${input}`); console.log(`[${ new Date().toUTCString() }] ${err}`); } this.deployment.currentOperationNumber++; this.deployment.progressOfDeleting += this.deployment.progressPerFile; this.updateProgress('progressOfDeleting'); this.deployment.removeFile(); } async removeDirectory(input) { try { await this.connection.removeDir(input); console.log(`[${ new Date().toUTCString() }] DEL ${input}`); } catch (err) { console.log(`[${ new Date().toUTCString() }] ERROR REMOVE DIR: ${input}`); console.log(`[${ new Date().toUTCString() }] ${err}`); } this.deployment.currentOperationNumber++; this.deployment.progressOfDeleting += this.deployment.progressPerFile; this.updateProgress('progressOfDeleting'); this.deployment.removeFile(); } updateProgress (progressType) { process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(this.deployment[progressType]), operations: [this.deployment.currentOperationNumber, this.deployment.operationsCounter] } }); } async testConnection(app, deploymentConfig, siteName, uuid) { let client = new ftp.Client(15000); client.ftp.verbose = true; client.ftp.log = this.connectionDebugger; let waitForTimeout = true; let ftpPassword = deploymentConfig.password; let account = slug(siteName); let secureConnection = false; if (uuid) { account = uuid; } if(ftpPassword === 'publii ' + account) { ftpPassword = await passwordSafeStorage.getPassword('publii', account); } if(deploymentConfig.protocol !== 'ftp') { secureConnection = true; } let connectionParams = { host: deploymentConfig.server, port: deploymentConfig.port, user: deploymentConfig.username, password: ftpPassword, secure: secureConnection, secureOptions: { host: deploymentConfig.server, port: deploymentConfig.port, user: deploymentConfig.username, password: ftpPassword, rejectUnauthorized: deploymentConfig.rejectUnauthorized } }; let testFilePath = normalizePath(path.join(app.sitesDir, siteName, 'input', 'publii.test')); fs.writeFileSync(testFilePath, 'It is a test file. You can remove it.'); try { await client.access(connectionParams); waitForTimeout = false; } catch (err) { client.close(); app.mainWindow.webContents.send('app-deploy-test-error', { message: stripTags((err.message).toString()) }); } try { await client.uploadFrom(normalizePath(testFilePath), normalizePath(path.join(deploymentConfig.path, 'publii.test'))); } catch (err) { app.mainWindow.webContents.send('app-deploy-test-write-error'); if (fs.existsSync(testFilePath)) { fs.unlinkSync(testFilePath); } client.close(); return; } try { await client.remove(normalizePath(path.join(deploymentConfig.path, 'publii.test'))); } catch (err) { app.mainWindow.webContents.send('app-deploy-test-write-error'); if (fs.existsSync(testFilePath)) { fs.unlinkSync(testFilePath); } client.close(); return; } app.mainWindow.webContents.send('app-deploy-test-success'); if (fs.existsSync(testFilePath)) { fs.unlinkSync(testFilePath); } client.close(); setTimeout(function() { if (waitForTimeout === true) { client.close(); app.mainWindow.webContents.send('app-deploy-test-error'); } }, 15000); } connectionDebugger(message) { if(message.indexOf("PASS ") > -1) { message = '> PASS ******************************'; } message = `[${ new Date().toUTCString() }] ${message}`; console.log(message); } } module.exports = FTPAlt; ================================================ FILE: app/back-end/modules/deploy/ftp.js ================================================ /* * Class used to upload files to the FTP(S) server */ const fs = require('fs-extra'); const path = require('path'); const ftpClient = require('./../custom-changes/ftp'); const passwordSafeStorage = require('keytar'); const slug = require('./../../helpers/slug'); const normalizePath = require('normalize-path'); const stripTags = require('striptags'); class FTP { constructor(deploymentInstance = false) { this.deployment = deploymentInstance; this.connection = false; this.econnresetCounter = 0; this.softUploadErrors = {}; this.hardUploadErrors = []; } async initConnection() { let self = this; let waitForTimeout = true; let ftpPassword = this.deployment.siteConfig.deployment.password; let account = slug(this.deployment.siteConfig.name); let secureConnection = false; if (this.deployment.siteConfig.uuid) { account = this.deployment.siteConfig.uuid; } this.connection = new ftpClient(); if(ftpPassword === 'publii ' + account) { ftpPassword = await passwordSafeStorage.getPassword('publii', account); } if(this.deployment.siteConfig.deployment.protocol !== 'ftp') { secureConnection = true; } let connectionParams = { host: this.deployment.siteConfig.deployment.server, port: this.deployment.siteConfig.deployment.port, user: this.deployment.siteConfig.deployment.username, password: ftpPassword, secure: secureConnection, secureOptions: { host: this.deployment.siteConfig.deployment.server, port: this.deployment.siteConfig.deployment.port, user: this.deployment.siteConfig.deployment.username, password: ftpPassword, rejectUnauthorized: this.deployment.siteConfig.deployment.rejectUnauthorized }, connTimeout: 15000, pasvTimeout: 15000, debug: self.connectionDebugger.bind(self) }; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 6, operations: false } }); process.send({ type: 'web-contents', message: 'app-connection-in-progress' }); this.connection.connect(connectionParams); this.connection.on('ready', function() { waitForTimeout = false; process.send({ type: 'web-contents', message: 'app-connection-success' }); self.deployment.setInput(); self.deployment.setOutput(); self.deployment.prepareLocalFilesList(); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 7, operations: false } }); self.downloadFilesList(); }); this.connection.on('error', function(err) { console.log(`[${ new Date().toUTCString() }] FTP ERROR: ${err}`); if(typeof err === "string" && err.indexOf('ECONNRESET') > -1) { self.econnresetCounter++; if(self.econnresetCounter <= 5) { return; } } if(waitForTimeout) { waitForTimeout = false; self.connection.destroy(); process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: stripTags((err.message).toString()) } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); } }); this.connection.on('close', function(err) { console.log(`[${ new Date().toUTCString() }] FTP CONNECTION CLOSED: ${err}`); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); }); setTimeout(function() { if(waitForTimeout === true) { self.connection.destroy(); console.log(`[${ new Date().toUTCString() }] Request timeout...`); process.send({ type: 'web-contents', message: 'app-connection-error' }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); } }, 20000); } downloadFilesList() { let self = this; this.connection.get( normalizePath(path.join(this.deployment.outputDir, 'files.publii.json')), function(err, fileStream) { console.log(`[${ new Date().toUTCString() }] <- files.publii.json`); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8, operations: false } }); if (!err) { let fileStreamChunks = []; fileStream.on('data', (chunk) => { fileStreamChunks.push(chunk.toString()); }); fileStream.on('end', () => { let completeFile = fileStreamChunks.join(''); self.deployment.checkLocalListWithRemoteList(completeFile); }); } else { console.log(`[${ new Date().toUTCString() }] (!) ERROR WHILE DOWNLOADING files-remote.json`); console.log(`[${ new Date().toUTCString() }] ${err}`); self.deployment.compareFilesList(false); } } ); } uploadNewFileList() { let self = this; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 99, operations: [self.deployment.currentOperationNumber ,self.deployment.operationsCounter] } }); this.connection.put( normalizePath(path.join(this.deployment.inputDir, 'files.publii.json')), normalizePath(path.join(this.deployment.outputDir, 'files.publii.json')), function(err) { console.log(`[${ new Date().toUTCString() }] -> files.publii.json`); if (err) { console.log(`[${ new Date().toUTCString() }] ${err}`); } self.connection.logout(function(err) { if (err) { console.log(`[${ new Date().toUTCString() }] ${err}`); } self.connection.end(); self.connection.destroy(); }); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 100, operations: false } }); process.send({ type: 'sender', message: 'app-deploy-uploaded', value: { status: true, issues: self.hardUploadErrors.length > 0 } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); } ); } uploadFile(input, output) { let self = this; this.connection.put( input, output, function (err) { if (err) { console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD FILE: ${output}`); console.log(`[${ new Date().toUTCString() }] ${err}`); setTimeout(() => { if(!self.softUploadErrors[input]) { self.softUploadErrors[input] = 1; } else { self.softUploadErrors[input]++; } if(self.softUploadErrors[input] <= 5) { self.uploadFile(input, output); } else { self.hardUploadErrors.push(input); self.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] UPL HARD ERR ${input} -> ${output}`); self.deployment.progressOfUploading += self.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(self.deployment.progressOfUploading), operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter] } }); self.deployment.uploadFile(); } }, 500); } else { self.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${output}`); self.deployment.progressOfUploading += self.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(self.deployment.progressOfUploading), operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter] } }); self.deployment.uploadFile(); } } ); } uploadDirectory(input, output) { let self = this; this.connection.mkdir( output, true, function (err) { if (err) { console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD DIR: ${output}`); console.log(`[${ new Date().toUTCString() }] ${err}`); setTimeout(() => { if(!self.softUploadErrors[input]) { self.softUploadErrors[input] = 1; } else { self.softUploadErrors[input]++; } if(self.softUploadErrors[input] <= 5) { self.uploadDirectory(input, output); } else { self.hardUploadErrors.push(input); self.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] UPL HARD ERR ${input} -> ${output}`); self.deployment.progressOfUploading += self.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(self.deployment.progressOfUploading), operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter] } }); self.deployment.uploadFile(); } }, 500); } else { self.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${output}`); self.deployment.progressOfUploading += self.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(self.deployment.progressOfUploading), operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter] } }); self.deployment.uploadFile(); } } ); } removeFile(input) { let self = this; this.connection.delete( input, function (err) { self.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] DEL ${input}`); if (err) { console.log(`[${ new Date().toUTCString() }] ERROR REMOVE FILE: ${input}`); console.log(`[${ new Date().toUTCString() }] ${err}`); } self.deployment.progressOfDeleting += self.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(self.deployment.progressOfDeleting), operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter] } }); self.deployment.removeFile(); } ); } removeDirectory(input) { let self = this; this.connection.rmdir( input, true, function (err) { self.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] DEL ${input}`); if (err) { console.log(`[${ new Date().toUTCString() }] ERROR REMOVE DIR: ${input}`); console.log(`[${ new Date().toUTCString() }] ${err}`); } self.deployment.progressOfDeleting += self.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(self.deployment.progressOfDeleting), operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter] } }); self.deployment.removeFile(); } ); } async testConnection(app, deploymentConfig, siteName, uuid) { let client = new ftpClient(); let waitForTimeout = true; let ftpPassword = deploymentConfig.password; let account = slug(siteName); let secureConnection = false; if (uuid) { account = uuid; } if(ftpPassword === 'publii ' + account) { ftpPassword = await passwordSafeStorage.getPassword('publii', account); } if(deploymentConfig.protocol !== 'ftp') { secureConnection = true; } let connectionParams = { host: deploymentConfig.server, port: deploymentConfig.port, user: deploymentConfig.username, password: ftpPassword, secure: secureConnection, secureOptions: { host: deploymentConfig.server, port: deploymentConfig.port, user: deploymentConfig.username, password: ftpPassword, rejectUnauthorized: deploymentConfig.rejectUnauthorized }, connTimeout: 10000, pasvTimeout: 10000 }; let testFilePath = normalizePath(path.join(app.sitesDir, siteName, 'input', 'publii.test')); fs.writeFileSync(testFilePath, 'It is a test file. You can remove it.'); client.connect(connectionParams); client.on('ready', () => { waitForTimeout = false; client.put( normalizePath(testFilePath), normalizePath(path.join(deploymentConfig.path, 'publii.test')), (err) => { if (err) { app.mainWindow.webContents.send('app-deploy-test-write-error'); if (fs.existsSync(testFilePath)) { fs.unlinkSync(testFilePath); } client.destroy(); return; } client.delete( normalizePath(path.join(deploymentConfig.path, 'publii.test')), (err) => { if (err) { app.mainWindow.webContents.send('app-deploy-test-write-error'); if (fs.existsSync(testFilePath)) { fs.unlinkSync(testFilePath); } client.destroy(); return; } app.mainWindow.webContents.send('app-deploy-test-success'); if (fs.existsSync(testFilePath)) { fs.unlinkSync(testFilePath); } client.destroy(); } ); } ); }); if (fs.existsSync(testFilePath)) { fs.unlinkSync(testFilePath); } client.on('error', function(err) { if(waitForTimeout) { waitForTimeout = false; client.destroy(); app.mainWindow.webContents.send('app-deploy-test-error', { message: stripTags((err.message).toString()) }); } }); setTimeout(function() { if(waitForTimeout === true) { client.destroy(); app.mainWindow.webContents.send('app-deploy-test-error'); } }, 15000); } connectionDebugger(message) { if(message.indexOf("[connection] > 'PASS ") > -1) { message = '[connection] > PASS ******************************'; } message = `[${ new Date().toUTCString() }] ${message}`; console.log(message); } } module.exports = FTP; ================================================ FILE: app/back-end/modules/deploy/git.js ================================================ /* * Class used to upload files to the Github Pages */ const fs = require('fs-extra'); const gitClient = require('isomorphic-git') const http = require('isomorphic-git/http/node') const passwordSafeStorage = require('keytar'); const slug = require('./../../helpers/slug'); const stripTags = require('striptags'); class Git { constructor(deploymentInstance = false) { this.deployment = deploymentInstance; this.repositoryURL = ''; this.user = ''; this.password = ''; this.branch = ''; this.commitAuthor = ''; this.commitEmail = ''; this.commitMessage = ''; } async initConnection() { let account = slug(this.deployment.siteConfig.name); this.repositoryURL = this.deployment.siteConfig.deployment.git.url; this.user = this.deployment.siteConfig.deployment.git.user; this.password = this.deployment.siteConfig.deployment.git.password; this.branch = this.deployment.siteConfig.deployment.git.branch; this.commitAuthor = this.deployment.siteConfig.deployment.git.commitAuthor; this.commitEmail = this.deployment.siteConfig.deployment.git.commitEmail; this.commitMessage = this.deployment.siteConfig.deployment.git.commitMessage; if (this.deployment.siteConfig.uuid) { account = this.deployment.siteConfig.uuid; } if (this.password === 'publii-git-password ' + account) { this.password = await passwordSafeStorage.getPassword('publii-git-password', account); } process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 6, operations: false } }); process.send({ type: 'web-contents', message: 'app-connection-in-progress' }); this.deployment.setInput(); await this.deploy(); } async testConnection(app, deploymentConfig, siteName, uuid) { this.waitForTimeout = true; let account = slug(siteName); let username = deploymentConfig.git.user; let password = deploymentConfig.git.password; let url = deploymentConfig.git.url; if (uuid) { account = uuid; } if (password === 'publii-git-password ' + account) { password = await passwordSafeStorage.getPassword('publii-git-password', account); } let authObject = { username, password }; let timeoutCheck = setTimeout(function () { if(this.waitForTimeout === true) { app.mainWindow.webContents.send('app-deploy-test-error', { message: { translation: 'core.server.requestTimeout' } }); this.waitForTimeout = false; } }, 20000); try { await gitClient.getRemoteInfo({ http, url, onAuth: () => authObject, onAuthFailure: () => { app.mainWindow.webContents.send('app-deploy-test-error', { noAdditionalMessage: true, message: { translation: 'core.server.tokenOrServerAddressInvalid' } }); this.waitForTimeout = false; clearTimeout(timeoutCheck); }, onAuthSuccess: () => { app.mainWindow.webContents.send('app-deploy-test-success'); this.waitForTimeout = false; clearTimeout(timeoutCheck); } }); } catch (e) { console.log('Cannot connect to the git repository: ', e); if (e.data && e.data.response) { app.mainWindow.webContents.send('app-deploy-test-error', { message: stripTags((e.data.response).toString()) }); } else { app.mainWindow.webContents.send('app-deploy-test-error', { message: stripTags((e).toString()) }); } } } async prepareToSync (siteConfig, siteName, dir, sendProgressCallback) { let account = slug(siteName); let username = siteConfig.deployment.git.user; let password = siteConfig.deployment.git.password; let url = siteConfig.deployment.git.url; let branch = siteConfig.deployment.git.branch; let commitAuthor = siteConfig.deployment.git.commitAuthor; let commitEmail = siteConfig.deployment.git.commitEmail; if (siteConfig.uuid) { account = siteConfig.uuid; } if (password === 'publii-git-password ' + account) { password = await passwordSafeStorage.getPassword('publii-git-password', account); } let authObject = { username, password }; try { let repo = { fs, dir }; await gitClient.init({ fs, dir, defaultBranch: branch }); console.log('[i] Git debug: repository URL = ', url, ' branch = ' + branch, ' path = ' + dir); const hasProperOrigin = await this.hasCorrectOrigin(repo, url); if (!hasProperOrigin) { await gitClient.addRemote({ fs, dir, url, remote: 'origin' }); console.log('[i] Git debug: need to add origin'); await this.hasCorrectOrigin(repo, url); } else { console.log('[i] Git debug: not needed to add origin') } console.log('[i] Git debug: origin checked'); let info = await gitClient.getRemoteInfo({ http, url, onAuth: () => authObject }); console.log('[i] Git debug: remote info = ' + JSON.stringify(info)); await gitClient.fetch({ fs, http, dir, url, ref: branch, depth: 1, singleBranch: true, onMessage: message => { sendProgressCallback(1, message); console.log('[i] Git debug: ' + message); }, onProgress: event => { sendProgressCallback(1, event.phase + '(' + event.loaded + ')'); console.log('[i] Git debug: ' + event.phase + '(' + event.loaded + ')') }, onAuth: () => authObject }); await gitClient.checkout({ fs, dir, ref: branch, noCheckout: true, onProgress: event => { sendProgressCallback(1, event.phase + '(' + event.loaded + ')'); console.log('[i] Git debug: ' + event.phase + '(' + event.loaded + ')') }, }); await gitClient.pull({ http, fs, dir, ref: branch, remote: 'origin', author: { name: commitAuthor, email: commitEmail }, onMessage: message => { sendProgressCallback(1, message); console.log('[i] Git debug: ' + message); }, onProgress: event => { sendProgressCallback(1, event.phase + '(' + event.loaded + ')'); console.log('[i] Git debug: ' + event.phase + '(' + event.loaded + ')') }, onAuth: () => authObject }); console.log('[i] Git debug: pull done'); } catch (err) { console.log(`[${ new Date().toUTCString() }] ERROR: ${err}`); if (err.toString().indexOf('MergeNotSupportedError') > -1) { return 'merge-error'; } } return 'ok'; } async deploy() { try { let dir = this.deployment.inputDir; let repo = { fs, dir }; let authObject = { username: this.user, password: this.password }; await gitClient.statusMatrix(repo).then((status) => Promise.all( status.map(([filepath, , worktreeStatus]) => { if (filepath.substr(-9) === '.DS_Store' || filepath.substr(-9) === 'Thumbs.db') { console.log('[i] Git debug: skip system files'); } else { if (worktreeStatus) { gitClient.add({ ...repo, filepath }); console.log('[i] Git debug: add = ', filepath); } else { gitClient.remove({ ...repo, filepath }); console.log('[i] Git debug: remove = ', filepath); } } }) ) ); console.log('[i] Git debug: git add done'); const changesMatrix = await gitClient.statusMatrix(repo); const hasChanges = changesMatrix.some(row => { return row[1] !== 1 || row[2] !== 1; }); console.log('[i] Git debug: changes exists = ', hasChanges); if (hasChanges) { await gitClient.commit({ fs, dir, author: { name: this.commitAuthor, email: this.commitEmail }, message: this.commitMessage }); console.log('[i] Git debug: commit done'); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { message: 'Pushing changes to remote...', progress: 50, operations: [0, 1] } }); await gitClient.push({ http, fs, dir, remote: 'origin', ref: this.branch, onAuth: () => (authObject), onMessage: message => { console.log(`[i] Git debug - message: ${message}`); }, onProgress: event => { console.log(`[i] Git debug - event: ${event}`); } }); console.log('[i] Git debug: push done'); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { message: 'Push operation completed', progress: 99, operations: [2, 2] } }); } process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 100, operations: false } }); process.send({ type: 'sender', message: 'app-deploy-uploaded', value: { progress: 100, status: true } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); } catch (err) { console.log(`[${ new Date().toUTCString() }] ERROR: ${err}`); process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: 'Critical error: ' + stripTags((err).toString()) } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); } } async hasCorrectOrigin (repo, originToCheck) { let remotes = await gitClient.listRemotes(repo); console.log('[i] Git debug: remotes = ' + JSON.stringify(remotes)); if (!remotes) { return false; } for (let i = 0; i < remotes.length; i++) { if (remotes[i].url === originToCheck) { return true; } } return false; } } module.exports = Git; ================================================ FILE: app/back-end/modules/deploy/github-pages.js ================================================ /* * Class used to upload files to the Github Pages */ const fs = require('fs-extra'); const path = require('path'); const FileHelper = require('./../../helpers/file.js'); const passwordSafeStorage = require('keytar'); const slug = require('./../../helpers/slug'); const { Octokit } = require("@octokit/rest"); const list = require('ls-all'); const crypto = require('crypto'); const countFiles = require('count-files'); const moment = require('moment'); const normalizePath = require('normalize-path'); const stripTags = require('striptags'); class GithubPages { constructor(deploymentInstance = false) { this.deployment = deploymentInstance; this.serverURL = ''; if (this.deployment.siteConfig) { this.serverURL = this.deployment.siteConfig.deployment.github.server; } else { this.serverURL = this.deployment.github.server; } this.connection = false; this.client = new Octokit({ baseUrl: `https://${this.serverURL}`, request: { timeout: 30000 }, userAgent: "Publii" }); this.token = ''; this.repository = ''; this.user = ''; this.branch = ''; this.filesToUpdate = 0; this.filesUpdated = 0; this.waitForTimeout = false; this.uploadedBlobs = {}; } async initConnection() { let self = this; this.token = this.deployment.siteConfig.deployment.github.token; this.repository = this.deployment.siteConfig.deployment.github.repo; this.user = this.deployment.siteConfig.deployment.github.user; this.branch = 'heads/' + this.deployment.siteConfig.deployment.github.branch; this.parallelOperations = parseInt(this.deployment.siteConfig.deployment.github.parallelOperations, 10); this.apiRateLimiting = !!this.deployment.siteConfig.deployment.github.apiRateLimiting; this.waitForTimeout = true; let account = slug(this.deployment.siteConfig.name); if (this.deployment.siteConfig.uuid) { account = this.deployment.siteConfig.uuid; } if (this.token === 'publii-gh-token ' + account) { this.token = await passwordSafeStorage.getPassword('publii-gh-token', account); } this.client = new Octokit({ auth: this.token, baseUrl: `https://${this.serverURL}`, request: { timeout: 30000 }, userAgent: "Publii" }); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 6, operations: false } }); process.send({ type: 'web-contents', message: 'app-connection-in-progress' }); self.deployment.setInput(); self.deployment.setOutput(true); /* * Create CNAME file if necessary */ if (this.deployment.siteConfig.domain.indexOf('github.io') === -1) { let cnameFilePath = path.join(self.deployment.inputDir, 'CNAME'); let domainName = this.deployment.siteConfig.domain; if (domainName.indexOf('//') > -1) { domainName = domainName.split('//')[1]; } if (domainName.indexOf('/') === 0) { domainName = domainName.slice(1); } fs.writeFileSync( cnameFilePath, domainName ); } countFiles(self.deployment.inputDir, async function (err, results) { let numberOfFiles = parseInt(results.files + results.dirs, 10); if(numberOfFiles > 4000) { process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: { translation: 'core.server.tooManyFilesInfo', translationVars: { numberOfFiles: numberOfFiles } } } }); return; } try { if (self.apiRateLimiting) { let result = self.getAPIRateLimit(); if(result.remaining < 10) { process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: { translation: 'core.server.requestLimitExceededInfo', translationVars: { remaining: parseInt(result.remaining, 10), resetTime: moment(parseInt(result.reset * 1000, 10)).format('MMMM Do YYYY, h:mm:ss a') } } } }); return; } } await self.deploy(); } catch (err) { console.log(`[${ new Date().toUTCString() }] ERROR: ${JSON.stringify(err)}`); process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: 'E2 ' + stripTags((err).toString()) } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); } }); setTimeout(function() { if(this.waitForTimeout === true) { process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: { translation: 'core.server.requestTimeout' } } }); } }, 15000); } async testConnection(app, deploymentConfig, siteName, uuid) { let token = deploymentConfig.github.token; let repository = deploymentConfig.github.repo; let user = deploymentConfig.github.user; let branch = 'heads/' + deploymentConfig.github.branch; let account = slug(siteName); this.waitForTimeout = true; if (uuid) { account = uuid; } if(token === 'publii-gh-token ' + account) { token = await passwordSafeStorage.getPassword('publii-gh-token', account); } this.client = new Octokit({ auth: token, baseUrl: `https://${this.serverURL}`, request: { timeout: 30000 }, userAgent: "Publii" }); this.apiRequest( { owner: user, repo: repository, ref: branch }, (api) => api.rest.git.getRef, (result) => { if(result.data && result.data.object) { return result.data.object.sha; } return null; } ).then(result => { if(result === null) { this.waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-error', { message: { translation: 'core.server.branchDoesNotExist' } }); return; } this.waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-success'); }).catch(err => { err = JSON.parse(err); this.waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-error', { message: stripTags((err.message).toString()) }); }); setTimeout(function() { if(this.waitForTimeout === true) { app.mainWindow.webContents.send('app-deploy-test-error', { message: { translation: 'core.server.requestTimeout' } }); this.waitForTimeout = false; } }, 10000); } async deploy() { let self = this; try { let commitSHA; this.uploadedBlobs = {}; commitSHA = await this.getLatestSHA(); let treeSHA = await this.getTreeSHA(commitSHA); let remoteTree = await this.getTreeData(treeSHA); let trees = await this.listFolderFiles(remoteTree); let finalTree = await this.getNewTreeBasedOnDiffs(trees.remoteTree, trees.localTree); finalTree = await this.createBlobs(finalTree, false); finalTree = await this.updateBlobsList(finalTree); let sha = await this.createTree(finalTree); sha = await this.createCommit(sha, commitSHA); let result = await this.createReference(sha); if(result === false) { setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); return; } process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 100, operations: false } }); process.send({ type: 'sender', message: 'app-deploy-uploaded', value: { progress: 100, status: true } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); } catch (err) { console.log(`[${ new Date().toUTCString() }] ERROR: ${JSON.stringify(err)}`); process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: 'E1 ' + stripTags(JSON.stringify(err)) } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); } } async apiRequest(requestData, method, extractor) { try { let data = await method(this.client)(requestData); let result = extractor ? extractor(data) : data; return result; } catch (err) { console.log(`[${ new Date().toUTCString() }] (i) TRIED AGAIN: ${method.toString()} - ${requestData.filePath}`); try { let data = await method(this.client)(requestData); let result = extractor ? extractor(data) : data; return result; } catch (retryErr) { console.log(`[${ new Date().toUTCString() }] (i) TRIED AGAIN FAIL: ${method.toString()} - ${requestData.filePath}`); throw retryErr; } } } getAPIRateLimit() { return this.apiRequest( {}, (api) => api.rest.rateLimit.get, (result) => result.data.resources.core ); } getLatestSHA() { process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8, message: 'core.server.getInfoAboutLatestCommit' } }); return this.apiRequest( { owner: this.user, repo: this.repository, ref: this.branch }, (api) => api.rest.git.getRef, (result) => { this.waitForTimeout = false; if(result.data && result.data.object) { return result.data.object.sha; } return null; } ); } getTreeSHA(latestCommitSHA) { process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8, message: 'core.server.retrievingHandlerOfRemoteFilesTree' } }); return this.apiRequest( { owner: this.user, repo: this.repository, commit_sha: latestCommitSHA }, (api) => api.rest.git.getCommit, (result) => result.data.tree.sha ); } getTreeData(treeSHA) { process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8, message: { translation: 'core.server.retrievingRemoteFilesTree' } } }); return this.apiRequest( { owner: this.user, repo: this.repository, tree_sha: treeSHA, recursive: 1 }, (api) => api.rest.git.getTree, (result) => result.data.tree ); } async getNewTreeBasedOnDiffs(remoteTree, localTree) { process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8, message: { translation: 'core.server.preparingFilesTreeToUpload' } } }); this.filesToUpdate = 0; this.filesUpdated = 0; for(let localFile of localTree) { let remoteFile = this.findRemoteFile(localFile.path, remoteTree); if(remoteFile === false) { localFile.getBlob = true; this.filesToUpdate++; continue; } if(localFile.sha === false) { if(remoteFile.size !== localFile.size) { localFile.getBlob = true; this.filesToUpdate++; continue; } localFile.sha = remoteFile.sha; continue; } if(localFile.sha !== remoteFile.sha) { localFile.getBlob = true; this.filesToUpdate++; continue; } } return localTree; } findRemoteFile(filePath, remoteTree) { for(let remoteFile of remoteTree) { if(remoteFile.path === filePath) { return remoteFile; } } return false; } async listFolderFiles(remoteTree) { return list([this.deployment.inputDir], { recurse: true, flatten: true }).then(files => { let localTree = files.filter(file => !file.mode.dir) .map(file => { let calculatedHash = false; let fileSize = fs.statSync(file.path).size; if(!this.isBinaryFile(file.path)) { let fileContent = FileHelper.readFileSync(file.path); fileSize = fileContent.length; calculatedHash = crypto.createHash('sha1') .update("blob " + fileSize + "\0" + fileContent) .digest('hex'); } return { fullPath: normalizePath(file.path), path: normalizePath(path.relative(this.deployment.inputDir, file.path)), mode: file.mode.exec ? '100755' : '100644', type: 'blob', size: fileSize, sha: calculatedHash, encoding: 'base64', getBlob: false }; }) .filter(file => this.isNecessaryFile(file.path)); return { localTree: localTree, remoteTree: remoteTree }; }); } createBlob(filePath) { let fileContent = FileHelper.readFileSync(filePath, { encoding: 'base64' }); console.log(`[${ new Date().toUTCString() }] CREATE BLOB: ${filePath}`); return this.apiRequest( { owner: this.user, repo: this.repository, encoding: 'base64', content: fileContent, filePath: filePath }, (api) => api.rest.git.createBlob, (result) => { this.uploadedBlobs[filePath] = result.data.sha; console.log(`[${ new Date().toUTCString() }] CREATED BLOB: ${filePath} - ${result.data.sha}`); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + (Math.floor((this.filesUpdated / this.filesToUpdate) * 100) - 8), operations: [this.filesUpdated, this.filesToUpdate] } }); this.filesUpdated++; return result.data.sha; } ); } updateBlobsList(files) { let counterOfFilesToUpload = 0; let output = files.map(file => { if(this.uploadedBlobs[file.fullPath]) { file.sha = this.uploadedBlobs[file.fullPath]; file.getBlob = false; } if(file.getBlob) { counterOfFilesToUpload++; } return file; }); return output; } async createBlobs(files, reuploadSession = false) { if (this.apiRateLimiting) { let result = await this.getAPIRateLimit(); if(result.remaining < this.filesToUpdate + 10) { process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: { translation: 'core.server.requestLimitExceededInfo', translationVars: { remaining: parseInt(result.remaining, 10), resetTime: moment(parseInt(result.reset * 1000, 10)).format('MMMM Do YYYY, h:mm:ss a') } } } }); return []; } } let filesToUpdate = []; for (let i = 0; i < files.length; i++) { let file = files[i]; if(file.getBlob) { filesToUpdate.push(i); } } for (let i = 0; i < filesToUpdate.length; i += this.parallelOperations) { let requests = []; for (let j = 0; j < this.parallelOperations; j++) { let index = filesToUpdate[i + j]; if (typeof index === 'number') { let file = files[index]; requests.push(this.createBlob(file.fullPath).then((sha) => { file = Object.assign({}, file, { sha: sha, getBlob: false }); })); } } await Promise.all(requests); } return files; } createTree(tree) { if(!tree || !tree.length) { return []; } process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + (Math.floor((this.filesUpdated / this.filesToUpdate) * 100) - 8), operations: [this.filesUpdated, this.filesToUpdate] } }); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 95, message: { translation: 'core.server.creatingNewRemoteFilesTree' } } }); let logPath = path.join(this.deployment.appDir, 'github-tree.txt'); fs.writeFileSync(logPath, JSON.stringify(tree, null, 4)); return this.apiRequest( { owner: this.user, repo: this.repository, tree: tree }, (api) => api.rest.git.createTree, (result) => result.data.sha ); } createCommit(tree, parentSHA) { if(!tree.length) { return ''; } process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 95, message: { translation: 'core.server.creatingNewRemoteFilesTree' } } }); return this.apiRequest( { owner: this.user, repo: this.repository, message: 'Updated from Publii', tree: tree, parents: [parentSHA] }, (api) => api.rest.git.createCommit, (result) => result.data.sha ); } createReference(sha) { if(sha === '') { return false; } process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 99, message: { translation: 'core.server.finishingDeploymentProcess' } } }); return this.apiRequest( { owner: this.user, repo: this.repository, sha: sha, ref: this.branch }, (api) => api.rest.git.updateRef ); } isBinaryFile(fullPath) { let extension = path.parse(fullPath).ext; let nonBinaryExtensions = [ '.html', '.htm', '.xml', '.json', '.css', '.js', '.map', '.svg' ]; if(nonBinaryExtensions.indexOf(extension) > -1) { return false; } return true; } isNecessaryFile(filePath) { let filename = path.parse(filePath).base; let unnecessaryFiles = [ '.DS_Store', 'thumbs.db' ]; if(unnecessaryFiles.indexOf(filename) > -1) { return false; } return true; } } module.exports = GithubPages; ================================================ FILE: app/back-end/modules/deploy/gitlab-pages.js ================================================ /* * Class used to upload files to the Github Pages */ const path = require('path'); const FileHelper = require('./../../helpers/file.js'); const passwordSafeStorage = require('keytar'); const slug = require('./../../helpers/slug'); const { Gitlab } = require('@gitbeaker/node'); const stripTags = require('striptags'); class GitlabPages { constructor (deploymentInstance = false) { this.deployment = deploymentInstance; this.connection = false; this.repository = ''; this.user = ''; this.branch = ''; this.projectID = ''; this.filesToUpdate = 0; this.filesUpdated = 0; this.waitForTimeout = false; this.uploadedBlobs = {}; this.remoteFilesList = []; this.filesToRemove = []; this.filesToUpdate = []; this.filesToUpload = []; this.binaryFilesToUpdate = []; this.binaryFilesToUpload = []; this.binaryProgressOffset = 0; this.binaryFilesUploadedCount = 0; this.binaryFilesToUploadCount = 0; this.currentUploadProgress = 0; } async testConnection (app, deploymentConfig, siteName, uuid) { let repository = deploymentConfig.gitlab.repo; let branchName = deploymentConfig.gitlab.branch; let token = deploymentConfig.gitlab.token; let account = slug(siteName); this.waitForTimeout = true; if (uuid) { account = uuid; } if (token === 'publii-gl-token ' + account) { token = await passwordSafeStorage.getPassword('publii-gl-token', account); } this.client = new Gitlab({ host: deploymentConfig.gitlab.server, token: token, rejectUnauthorized: deploymentConfig.rejectUnauthorized }); this.client.Projects.all({ owned: true, maxPages: 1, perPage: 1 }).then(project => { this.client.Projects.all({ search: repository, owned: true, maxPages: 1, perPage: 1 }).then(projects => { let projectID = projects[0].id; // Detect a case when repository name is only similar to the provided repository name (not equal) if (projects[0].name !== repository && projects[0].path !== repository) { this.waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-error', { message: { translation: 'core.server.repositoryDoesNotExist' } }); return; } if(!projectID) { this.waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-error', { message: { translation: 'core.server.repositoryDoesNotExist' } }); return; } this.client.Branches.show(projectID, branchName).then(branch => { if(!branch) { this.waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-error', { message: { translation: 'core.server.branchDoesNotExist' } }); return; } this.waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-success'); }).catch(err => { this.waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-error', { message: { translation: 'core.server.branchDoesNotExist' } }); }); }).catch(err => { this.waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-error', { message: { translation: 'core.server.repositoryDoesNotExist' } }); }); }).catch(err => { this.waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-error', { message: { translation: 'core.server.tokenOrServerAddressInvalid' } }); }); setTimeout(function() { if(this.waitForTimeout === true) { app.mainWindow.webContents.send('app-deploy-test-error', { message: { translation: 'core.server.requestTimeout' } }); this.waitForTimeout = false; } }, 10000); } async initConnection () { this.repository = this.deployment.siteConfig.deployment.gitlab.repo; this.user = this.deployment.siteConfig.deployment.gitlab.user; this.branch = this.deployment.siteConfig.deployment.gitlab.branch; this.remoteFilesList = []; this.filesToRemove = []; this.filesToUpdate = []; this.filesToUpload = []; this.waitForTimeout = true; let token = this.deployment.siteConfig.deployment.gitlab.token; let account = slug(this.deployment.siteConfig.name); if (this.deployment.siteConfig.uuid) { account = this.deployment.siteConfig.uuid; } if (token === 'publii-gl-token ' + account) { token = await passwordSafeStorage.getPassword('publii-gl-token', account); } this.client = new Gitlab({ host: this.deployment.siteConfig.deployment.gitlab.server, token: token, rejectUnauthorized: this.deployment.siteConfig.deployment.rejectUnauthorized }); this.setUploadProgress(6); console.log(`[${ new Date().toUTCString() }] (!) CLIENT CREATED`); process.send({ type: 'web-contents', message: 'app-connection-in-progress' }); this.deployment.setInput(); this.deployment.setOutput(true); this.deployment.prepareLocalFilesList(); this.setUploadProgress(7); this.downloadFilesList(); } downloadFilesList () { this.client.Projects.all({ search: this.repository, owned: true, maxPages: 1, perPage: 1 }).then(projects => { this.projectID = projects[0].id; this.client.RepositoryFiles.showRaw(this.projectID, 'publii-files.json', this.branch).then(response => { let remoteListToCheck = ''; if (typeof response === 'Buffer') { remoteListToCheck = response.toString(); } else { remoteListToCheck = response; } try { if (remoteListToCheck.length) { this.remoteFilesList = JSON.parse(remoteListToCheck); } else { this.remoteFilesList = []; } } catch (e) { this.remoteFilesList = []; } this.deployment.checkLocalListWithRemoteList(response); console.log(`[${ new Date().toUTCString() }] (!) REMOTE FILE DOWNLOADED`); }).catch(err => { console.log(`[${ new Date().toUTCString() }] (!) REMOTE FILE NOT DOWNLOADED`); console.log(`[${ new Date().toUTCString() }] (!) ERROR WHILE DOWNLOADING publii-files.json: ${err.error}`); this.deployment.compareFilesList(false); }); }).catch(err => { console.log(`[${ new Date().toUTCString() }] downloadFilesList: ${err.error}`); console.warn(`[${ new Date().toUTCString() }] ${err}`); process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: stripTags((err.message).toString()) } }); }); } startSync () { this.setUploadProgress(8); this.removeFiles(); } removeFiles () { // Create a commit to remove all unnecessary files if (this.deployment.filesToRemove.length) { this.filesToRemove = []; console.log(`[${ new Date().toUTCString() }] (!) FILES TO REMOVE DETECTED`); for (let i = 0; i < this.deployment.filesToRemove.length; i++) { let filePath = this.deployment.filesToRemove[i].path; this.filesToRemove.push({ 'action': 'delete', 'file_path': this.getPrefix(filePath) + filePath }); } return this.makeCommit(this.filesToRemove, this.updateTextFiles.bind(this), '[skip ci] Publii - remove files'); } console.log(`[${ new Date().toUTCString() }] (!) NO FILES TO REMOVE DETECTED`); return this.updateTextFiles(); } updateTextFiles () { this.setUploadProgress(10); // Create a commit to update all non-binary files if (this.deployment.filesToUpload.length) { this.filesToUpdate = []; let existingFilesList = this.remoteFilesList.map(file => file.path); for (let i = 0; i < this.deployment.filesToUpload.length; i++) { if (existingFilesList.indexOf(this.deployment.filesToUpload[i].path) === -1) { continue; } if (this.isBinaryFile(this.deployment.filesToUpload[i].path)) { continue; } if (!this.isNecessaryFile(this.deployment.filesToUpload[i].path)) { continue; } let filePath = this.deployment.filesToUpload[i].path; this.filesToUpdate.push({ 'action': 'update', 'file_path': this.getPrefix(filePath) + filePath, 'encoding': 'base64', 'content': this.readFile(path.join(this.deployment.inputDir, this.deployment.filesToUpload[i].path)) }); } if (this.filesToUpdate.length) { console.log(`[${ new Date().toUTCString() }] (!) TEXT FILES TO UPDATE DETECTED`); return this.makeCommit(this.filesToUpdate, this.uploadTextFiles.bind(this), '[skip ci] Publii - update non-binary files'); } } console.log(`[${ new Date().toUTCString() }] (!) NO TEXT FILES TO UPDATE DETECTED`); this.uploadTextFiles(); } uploadTextFiles () { this.setUploadProgress(12); // Create a commit to upload all non-binary files if (this.deployment.filesToUpload.length) { this.filesToUpdate = []; let existingFilesList = this.remoteFilesList.map(file => file.path); for (let i = 0; i < this.deployment.filesToUpload.length; i++) { if (existingFilesList.indexOf(this.deployment.filesToUpload[i].path) > -1) { continue; } if (this.isBinaryFile(this.deployment.filesToUpload[i].path)) { continue; } if (!this.isNecessaryFile(this.deployment.filesToUpload[i].path)) { continue; } let filePath = this.deployment.filesToUpload[i].path; this.filesToUpdate.push({ 'action': 'create', 'file_path': this.getPrefix(filePath) + filePath, 'encoding': 'base64', 'content': this.readFile(path.join(this.deployment.inputDir, this.deployment.filesToUpload[i].path)) }); } if (this.filesToUpdate.length) { console.log(`[${ new Date().toUTCString() }] (!) TEXT FILES TO UPLOAD DETECTED`); return this.makeCommit(this.filesToUpdate, this.createBinaryFilesList.bind(this), '[skip ci] Publii - upload non-binary files'); } } console.log(`[${ new Date().toUTCString() }] (!) NO TEXT FILES TO UPLOAD DETECTED`); this.createBinaryFilesList(); } createBinaryFilesList () { this.setUploadProgress(15); this.binaryFilesToUpdate = []; this.binaryFilesToUpload = []; if (this.deployment.filesToUpload.length) { let existingFilesList = this.remoteFilesList.map(file => file.path); for (let i = 0; i < this.deployment.filesToUpload.length; i++) { if (existingFilesList.indexOf(this.deployment.filesToUpload[i].path) > -1) { if (this.isBinaryFile(this.deployment.filesToUpload[i].path)) { console.log(`[${ new Date().toUTCString() }] (!) BINARY FILE TO UPDATE DETECTED`); let filePath = this.deployment.filesToUpload[i].path; this.binaryFilesToUpdate.push({ 'action': 'update', 'file_path': this.getPrefix(filePath) + filePath, 'encoding': 'base64' }); } } else { if (this.isBinaryFile(this.deployment.filesToUpload[i].path)) { console.log(`[${ new Date().toUTCString() }] (!) BINARY FILE TO UPLOAD DETECTED`); let filePath = this.deployment.filesToUpload[i].path; this.binaryFilesToUpload.push({ 'action': 'create', 'file_path': this.getPrefix(filePath) + filePath, 'encoding': 'base64' }); } } } } this.binaryProgressOffset = 82 / (this.binaryFilesToUpdate.length + this.binaryFilesToUpload.length); this.binaryFilesUploadedCount = 0; this.binaryFilesToUploadCount = this.binaryFilesToUpdate.length; this.currentUploadProgress = 15; this.updateBinaryFiles(); } updateBinaryFiles () { if (this.binaryFilesToUpdate.length) { let commits = []; let progress = this.currentUploadProgress; for (let i = 1; i <= 10 && this.binaryFilesToUpdate.length; i++) { progress = progress + this.binaryProgressOffset; this.binaryFilesUploadedCount++; let commit = this.binaryFilesToUpdate.shift(); let fixedPath = commit.file_path.indexOf('public/') === 0 ? commit.file_path.substr(6) : commit.file_path; commit.content = this.readFile(path.join(this.deployment.inputDir, fixedPath)); commits.push(commit); } let operations = [this.binaryFilesUploadedCount, this.binaryFilesToUploadCount]; this.setUploadProgress(progress, operations); console.log(`[${ new Date().toUTCString() }] (!) BINARY FILES UPDATED`); this.makeCommit(commits, this.updateBinaryFiles.bind(this), '[skip ci] Publii - update ' + commits.length + ' files'); return; } this.binaryFilesUploaded = 0; this.binaryFilesToUploadCount = this.binaryFilesToUpload.length; this.uploadBinaryFiles(); } uploadBinaryFiles () { if (this.binaryFilesToUpload.length) { let commits = []; let progress = this.currentUploadProgress; for (let i = 1; i <= 10 && this.binaryFilesToUpload.length; i++) { progress = progress + this.binaryProgressOffset; this.binaryFilesUploadedCount++; let commit = this.binaryFilesToUpload.shift(); let fixedPath = commit.file_path.indexOf('public/') === 0 ? commit.file_path.substr(6) : commit.file_path; commit.content = this.readFile(path.join(this.deployment.inputDir, fixedPath)); commits.push(commit); } let operations = [this.binaryFilesUploadedCount, this.binaryFilesToUploadCount]; this.setUploadProgress(progress, operations); console.log(`[${ new Date().toUTCString() }] (!) BINARY FILES UPLOADED`); this.makeCommit(commits, this.uploadBinaryFiles.bind(this), '[skip ci] Publii - upload ' + commits.length + ' files'); return; } this.updateFilesListFile(); } updateFilesListFile () { this.setUploadProgress(98); let localFilesListPath = path.join(this.deployment.inputDir, 'files.publii.json'); let localFilesContent = FileHelper.readFileSync(localFilesListPath); let actionType = 'create'; let commit = []; if (this.remoteFilesList.length) { console.log(`[${ new Date().toUTCString() }] (!) REMOTE FILES SHOULD BE UPDATED`); actionType = 'update'; } commit.push({ 'action': actionType, 'file_path': 'publii-files.json', 'encoding': 'base64', 'content': Buffer.from(localFilesContent).toString('base64') }); console.log(`[${ new Date().toUTCString() }] (!) REMOTE FILES LIST UPDATED`); return this.makeCommit(commit, this.finishSync.bind(this), 'Publii - upload remote files list'); } finishSync () { this.setUploadProgress(100); process.send({ type: 'sender', message: 'app-deploy-uploaded', value: { status: true } }); } makeCommit (operations, nextOperationCallback, commitMessage = 'Publii - deployment') { this.client.Commits.create(this.projectID, this.branch, commitMessage, operations).then(res => { return nextOperationCallback(); }).catch(err => { console.log(`[${ new Date().toUTCString() }] (!) COMMIT ERROR: ${JSON.stringify(err)}`); process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: stripTags((err.message).toString()) } }); }); } setUploadProgress (progress, operations = false) { this.currentUploadProgress = progress; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: progress, operations: operations } }); } readFile (filePath) { return Buffer.from(FileHelper.readFileSync(filePath)).toString('base64'); } isBinaryFile (fullPath) { let extension = path.parse(fullPath).ext; let nonBinaryExtensions = [ '.html', '.htm', '.xml', '.json', '.css', '.js', '.map', '.svg' ]; if(nonBinaryExtensions.indexOf(extension) > -1) { return false; } return true; } getPrefix (fileNameToBePrefixed) { let prefix = 'public'; if (fileNameToBePrefixed[0] !== '/') { prefix = 'public/'; } return prefix; } isNecessaryFile (filePath) { let filename = path.parse(filePath).base; if(filename.substr(0,1) === '.') { return false; } return true; } } module.exports = GitlabPages; ================================================ FILE: app/back-end/modules/deploy/google-cloud.js ================================================ /* * Class used to upload files to the FTP(S) server */ const fs = require('fs-extra'); const path = require('path'); const FileHelper = require('./../../helpers/file.js'); const { Storage } = require('@google-cloud/storage'); const normalizePath = require('normalize-path'); const stripTags = require('striptags'); class GoogleCloud { constructor(deploymentInstance = false) { this.deployment = deploymentInstance; this.connection = false; this.debugOutput = []; this.econnresetCounter = 0; this.softUploadErrors = {}; this.hardUploadErrors = []; } async initConnection() { let self = this; let bucketName = this.deployment.siteConfig.deployment.google.bucket; let keyFilePath = normalizePath(this.deployment.siteConfig.deployment.google.key); this.prefix = this.deployment.siteConfig.deployment.google.prefix; if(!fs.existsSync(keyFilePath)) { process.send({ type: 'web-contents', message: 'app-connection-error' }); return; } let keyData = require(keyFilePath); let gcs = new Storage({ projectId: keyData.project_id, credentials: { client_email: keyData.client_email, private_key: keyData.private_key } }); this.connection = gcs.bucket(bucketName); this.connection.setMetadata({ website: { mainPageSuffix: "index.html", notFoundPage: "404.html" } }); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 6, operations: false } }); process.send({ type: 'web-contents', message: 'app-connection-in-progress' }); process.send({ type: 'web-contents', message: 'app-connection-success' }); self.deployment.setInput(); self.deployment.setOutput(true); self.deployment.prepareLocalFilesList(); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 7, operations: false } }); self.downloadFilesList(); } downloadFilesList() { let self = this; let fileToDownload = normalizePath(path.join(this.deployment.outputDir, 'files.publii.json')); if(typeof this.prefix === 'string' && this.prefix !== '') { fileToDownload = normalizePath(path.join(this.deployment.outputDir, this.prefix, 'files.publii.json')); } this.connection.file(fileToDownload).download({ destination: path.join(self.deployment.configDir, 'temp-files-remote.json') }, function(err) { if (!err) { let downloadedFilePath = path.join(self.deployment.configDir, 'temp-files-remote.json'); let downloadedFile = FileHelper.readFileSync(downloadedFilePath); self.deployment.checkLocalListWithRemoteList(downloadedFile); } else { self.deployment.compareFilesList(false); } }); } uploadNewFileList() { let self = this; let fileToUpload = normalizePath(path.join(this.deployment.inputDir, 'files.publii.json')); let fileDestination = 'files.publii.json'; if(typeof this.prefix === 'string' && this.prefix !== '') { fileDestination = normalizePath(path.join(this.prefix, 'files.publii.json')); } process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 99, operations: [self.deployment.currentOperationNumber ,self.deployment.operationsCounter] } }); this.connection.upload(fileToUpload, { destination: fileDestination }, function(err) { console.log(`[${ new Date().toUTCString() }] -> files.publii.json`); if (err) { console.log(`[${ new Date().toUTCString() }] ${err}`); } process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 100, operations: false } }); process.send({ type: 'sender', message: 'app-deploy-uploaded', value: { status: true } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); }); } uploadFile(input, output) { let self = this; if (typeof this.prefix === 'string' && this.prefix !== '') { output = normalizePath(path.join(this.prefix, output)); } if (output[0] === '/') { output = output.substr(1); } this.connection.upload(input, { destination: output, public: true }, function(err) { if (err) { console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD FILE: ${output}`); console.log(`[${ new Date().toUTCString() }] ${err}`); setTimeout(() => { if(!self.softUploadErrors[input]) { self.softUploadErrors[input] = 1; } else { self.softUploadErrors[input]++; } if(self.softUploadErrors[input] <= 5) { self.uploadFile(input, output); } else { self.hardUploadErrors.push(input); self.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] UPL HARD ERR ${input} -> ${output}`); self.deployment.progressOfUploading += self.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(self.deployment.progressOfUploading), operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter] } }); self.deployment.uploadFile(); } }, 500); } else { self.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${output}`); self.deployment.progressOfUploading += self.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(self.deployment.progressOfUploading), operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter] } }); self.deployment.uploadFile(); } }); } uploadDirectory(input, output) { this.deployment.uploadFile(); } removeFile(input) { let self = this; if(typeof this.prefix === 'string' && this.prefix !== '') { input = normalizePath(path.join(this.prefix, input)); } if(input[0] === '/') { input = input.substr(1); } this.connection.file(input).delete(function (err) { self.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] DEL ${input}`); if (err) { console.log(`[${ new Date().toUTCString() }] ERROR REMOVE FILE: ${input}`); console.log(`[${ new Date().toUTCString() }] ${err}`); } self.deployment.progressOfDeleting += self.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(self.deployment.progressOfDeleting), operations: [self.deployment.currentOperationNumber ,self.deployment.operationsCounter] } }); self.deployment.removeFile(); }); } removeDirectory(input) { this.deployment.removeFile(); } async testConnection(app, deploymentConfig, siteName) { let bucketName = deploymentConfig.google.bucket; let keyFilePath = normalizePath(deploymentConfig.google.key); let waitForTimeout = true; if(!fs.existsSync(keyFilePath)) { waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-error'); return; } let keyData = require(keyFilePath); let gcs = new Storage({ projectId: keyData.project_id, credentials: { client_email: keyData.client_email, private_key: keyData.private_key } }); let bucket = gcs.bucket(bucketName); bucket.getMetadata().then(data => { waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-success'); }).catch(err => { waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-error', { message: stripTags((err.message).toString()) }); }); setTimeout(function() { if(waitForTimeout === true) { app.mainWindow.webContents.send('app-deploy-test-error'); } }, 15000); } } module.exports = GoogleCloud; ================================================ FILE: app/back-end/modules/deploy/libraries/netlify-api.js ================================================ const fs = require('fs'); const path = require('path'); const util = require('util'); const crypto = require('crypto'); const normalizePath = require('normalize-path'); const asyncReadFile = util.promisify(fs.readFile); class NetlifyAPI { constructor (settings, events = {}) { this.userAgent = 'Publii'; this.apiUrl = 'https://api.netlify.com/api/v1/'; this.accessToken = settings.accessToken || ''; this.siteID = settings.siteID || ''; this.inputDir = settings.inputDir || ''; this.events = { onStart: events.onStart || this.noop, onProgress: events.onProgress || this.noop, onError: events.onError || this.noop, onFinish: events.onFinish || this.noop, } } async deploy () { let localFilesList = await this.prepareLocalFilesList(); let deployData = await this.makeApiRequest('POST', 'sites/:site_id/deploys', localFilesList); let deployID = deployData.id; let hashesOfFilesToUpload = deployData.required; let filesToUpload = this.getFilesToUpload(localFilesList, hashesOfFilesToUpload); this.events.onStart(filesToUpload.length); for (let i = 0; i < filesToUpload.length; i++) { let filePath = filesToUpload[i]; try { let apiResponse = await this.uploadFile(filePath, deployID); if (!apiResponse) { return Promise.reject(apiResponse); } } catch (e) { try { let apiResponse = await this.uploadFile(filePath, deployID); if (!apiResponse) { return Promise.reject(apiResponse); } } catch (e) { return Promise.reject(false); } } this.events.onProgress(i); } this.events.onFinish(); return Promise.resolve(true); } async prepareLocalFilesList () { let tempFileList = this.readDirRecursiveSync(this.inputDir); let fileList = {}; for(let filePath of tempFileList) { // Skip directories if(fs.lstatSync(path.join(this.inputDir, filePath)).isDirectory()) { continue; } let fileHash = await this.getFileHash(path.join(this.inputDir, filePath)); let fileKey = ('/' + filePath).replace(/\/\//gmi, '/'); fileList[fileKey] = fileHash; } // Save the files list return Promise.resolve({ files: fileList }); } async makeApiRequest (method, endpoint, data) { let endpointUrl = this.apiUrl + endpoint.replace(':site_id', this.siteID); let headers = new Headers({ 'User-Agent': 'Publii', 'Authorization': `Bearer ${this.accessToken}`, 'Content-Type': 'application/json' }); let options = { method: method, headers: headers, body: JSON.stringify(data), }; if (method.toUpperCase() === 'GET') { delete options.body; } let fetchTimeoutPromise = new Promise((resolve, reject) => { let timeoutId = setTimeout(() => { clearTimeout(timeoutId); reject(new Error('Fetch request timeout')); }, 15000); }); try { let response = await Promise.race([fetch(endpointUrl, options), fetchTimeoutPromise]); if (response.ok) { return await response.json(); } else { let error = new Error(`Fetch HTTP Error: ${response.status}`); error.response = response; throw error; } } catch (error) { console.log(`Request failed (URL: ${endpointUrl}): ${error.message}`); } } async uploadFile (filePath, deployID) { let endpointUrl = this.apiUrl + 'deploys/' + deployID + '/files' + filePath; let fullFilePath = this.getFilePath(this.inputDir, filePath, true); let fileContent = await asyncReadFile(fullFilePath); let headers = new Headers({ 'User-Agent': 'Publii', 'Authorization': `Bearer ${this.accessToken}`, 'Content-Type': 'application/octet-stream', 'Content-Length': fileContent.length }); let options = { method: 'PUT', headers: headers, body: fileContent, }; let fetchTimeoutPromise = new Promise((resolve, reject) => { let timeoutId = setTimeout(() => { clearTimeout(timeoutId); reject(new Error('Fetch request timeout')); }, 15000); }); try { let response = await Promise.race([fetch(endpointUrl, options), fetchTimeoutPromise]); if (response.ok) { return await response.json(); } else { let error = new Error(`Fetch HTTP Error: ${response.status}`); error.response = response; throw error; } } catch (error) { console.log(`Request failed (URL: ${endpointUrl}): ${error.message}`); } return false; } getFilesToUpload (filesList, hashesToUpload) { let filePaths = Object.keys(filesList.files); let filesToUpload = []; let foundedHashes = []; for (let i = 0; i < filePaths.length; i++) { let filePath = filePaths[i]; if (hashesToUpload.indexOf(filesList.files[filePath]) > -1) { filesToUpload.push(filePath.replace(/\/\//gmi, '/')); foundedHashes.push(filesList.files[filePath]); } } return filesToUpload; } getFileHash (fileName) { return new Promise((resolve, reject) => { let shaSumCalculator = crypto.createHash('sha1'); try { let fileStream = fs.ReadStream(fileName); fileStream.on('data', fileContentChunk => shaSumCalculator.update(fileContentChunk)); fileStream.on('end', () => resolve(shaSumCalculator.digest('hex'))); } catch (error) { return reject(''); } }); } readDirRecursiveSync(dir, fileList) { let files = fs.readdirSync(dir); fileList = fileList || []; files.forEach(file => { if (this.fileIsDirectory(dir, file)) { fileList = this.readDirRecursiveSync(path.join(dir, file), fileList); return; } if (this.fileIsNotExcluded(file)) { fileList.push(this.getFilePath(dir, file)); } }); return fileList; }; fileIsDirectory (dir, file) { return fs.statSync(path.join(dir, file)).isDirectory(); } fileIsNotExcluded (file) { return file.indexOf('.') !== 0 || file === '.htaccess' || file === '.htpasswd' || file === '_redirects'; } getFilePath (dir, file, includeInputDir = false) { if (!includeInputDir) { dir = dir.replace(this.inputDir, '') } return normalizePath( path.join( dir, file ) ); } noop () { return false; } async testConnection () { let testData = await this.makeApiRequest('GET', 'sites/:site_id/'); if (testData.body && testData.id) { return Promise.resolve(true); } return Promise.reject(false); } } module.exports = NetlifyAPI; ================================================ FILE: app/back-end/modules/deploy/manual.js ================================================ /* * Manual deployment class */ const fs = require('fs-extra'); const path = require('path'); const slug = require('./../../helpers/slug'); const archiver = require('archiver'); const Utils = require('./../../helpers/utils'); class ManualDeployment { constructor(deploymentInstance = false) { this.deployment = deploymentInstance; } async initConnection() { this.deployment.setInput(); this.deployment.prepareLocalFilesList(); switch(this.deployment.siteConfig.deployment.manual.output) { case 'catalog': this.returnCatalog(); break; case 'zip-archive': this.returnZipArchive(); break; case 'tar-archive': this.returnTarArchive(); break; default: setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); break; } } returnCatalog() { let outputBaseDir = this.deployment.siteConfig.deployment.manual.outputDirectory; let outputDirName = slug(this.deployment.siteName) + '-files'; if (outputBaseDir && !Utils.dirExists(outputBaseDir)) { process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: { translation: 'core.archive.destinationNotExists' } } }); return; } if (!outputBaseDir) { outputBaseDir = path.join(this.deployment.sitesDir, this.deployment.siteName); } let outputPath = path.join(outputBaseDir, outputDirName); if (outputPath !== '') { if (Utils.dirExists(outputPath)) { fs.emptyDirSync(outputPath); } fs.copy(this.deployment.inputDir, outputPath, { filter: (src, dest) => { if (src.substr(-9) === '.DS_Store' || src.substr(-9) === 'Thumbs.db') { return false; } return true; } }).then(() => this.endDeployment('catalog', outputPath)); return; } this.endDeployment('catalog', this.deployment.inputDir); } returnZipArchive() { let self = this; let backupFile = path.join( this.deployment.sitesDir, this.deployment.siteName, slug(this.deployment.siteName) + '.zip' ); if(this.deployment.siteConfig.deployment.manual.outputDirectory !== '') { backupFile = path.join(this.deployment.siteConfig.deployment.manual.outputDirectory, slug(this.deployment.siteName) + '-files.zip'); } let output = fs.createWriteStream(backupFile); let archive = archiver('zip'); output.on('error', function (err) { process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: { translation: 'core.archive.errorDuringCreatingZIP' } } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); }); output.on('close', function () { self.endDeployment('zip-archive', backupFile); }); archive.on('error', function (err) { process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: { translation: 'core.archive.errorDuringCreatingZIP' } } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); }); archive.pipe(output); archive.directory(this.deployment.inputDir, '/'); archive.finalize(); } returnTarArchive() { let self = this; let backupFile = path.join( this.deployment.sitesDir, this.deployment.siteName, slug(this.deployment.siteName) + '.tar' ); if(this.deployment.siteConfig.deployment.manual.outputDirectory !== '') { backupFile = path.join(this.deployment.siteConfig.deployment.manual.outputDirectory, slug(this.deployment.siteName) + '-files.tar'); } let output = fs.createWriteStream(backupFile); let archive = archiver('tar'); output.on('error', function (err) { process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: { translation: 'core.archive.errorDuringCreatingTAR' } } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); }); output.on('close', function () { self.endDeployment('tar-archive', backupFile); }); archive.on('error', function (err) { process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: { translation: 'core.archive.errorDuringCreatingTAR' } } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); }); archive.pipe(output); archive.directory(this.deployment.inputDir, '/'); archive.finalize(); } endDeployment(type, pathToOutput) { process.send({ type: 'web-contents', message: 'app-deploy-uploaded', value: { status: true, type: type, path: pathToOutput } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); } } module.exports = ManualDeployment; ================================================ FILE: app/back-end/modules/deploy/netlify.js ================================================ /* * Class used to upload files to the Netlify */ const fs = require('fs-extra'); const path = require('path'); const passwordSafeStorage = require('keytar'); const slug = require('./../../helpers/slug'); const NetlifyAPI = require('./libraries/netlify-api'); const stripTags = require('striptags'); class Netlify { constructor(deploymentInstance = false) { this.deployment = deploymentInstance; this.connection = false; this.debugOutput = []; } async initConnection() { let client; let localDir; let siteID = this.deployment.siteConfig.deployment.netlify.id; let token = this.deployment.siteConfig.deployment.netlify.token; let account = slug(this.deployment.siteConfig.name); if (this.deployment.siteConfig.uuid) { account = this.deployment.siteConfig.uuid; } if(siteID === 'publii-netlify-id ' + account) { siteID = await passwordSafeStorage.getPassword('publii-netlify-id', account); } if(token === 'publii-netlify-token ' + account) { token = await passwordSafeStorage.getPassword('publii-netlify-token', account); } this.deployment.setInput(); this.deployment.setOutput(true); localDir = this.deployment.inputDir; client = new NetlifyAPI({ accessToken: (token).toString().trim(), siteID: (siteID).toString().trim(), inputDir: localDir }, { onStart: this.onStart.bind(this), onProgress: this.onProgress.bind(this), onError: this.onError.bind(this) }); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 6, operations: false } }); process.send({ type: 'web-contents', message: 'app-connection-in-progress' }); let results = client.deploy(); results.then(res => { process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 100, operations: false } }); process.send({ type: 'sender', message: 'app-deploy-uploaded', value: { status: true } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); }).catch(err => { console.log(`[${ new Date().toUTCString() }] Netlify ERROR: ${err}`); this.onError(err); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); }); } onStart (totalFiles) { this.deployment.operationsCounter = parseInt(totalFiles, 10); this.deployment.progressPerFile = 90.0 / this.deployment.operationsCounter; this.deployment.currentOperationNumber = 0; this.deployment.progressOfUploading = 0; } onError (apiResponse = false) { if (typeof apiResponse === 'boolean' || !apiResponse.body) { process.send({ type: 'web-contents', message: 'app-connection-error' }); } else { process.send({ type: 'web-contents', message: 'app-connection-error', value: { additionalMessage: stripTags((JSON.parse(apiResponse.body).message).toString()) } }); } setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); } onProgress(currentFile) { if (currentFile < this.deployment.currentOperationNumber) { return; } this.deployment.currentOperationNumber = currentFile; this.deployment.progressOfUploading = this.deployment.currentOperationNumber * this.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(this.deployment.progressOfUploading), operations: [this.deployment.currentOperationNumber, this.deployment.operationsCounter] } }); } async testConnection(app, deploymentConfig, siteName, uuid) { let client; let siteID = deploymentConfig.netlify.id; let token = deploymentConfig.netlify.token; let account = slug(siteName); let waitForTimeout = true; if (uuid) { account = uuid; } if(siteID === 'publii-netlify-id ' + account) { siteID = await passwordSafeStorage.getPassword('publii-netlify-id', account); } if(token === 'publii-netlify-token ' + account) { token = await passwordSafeStorage.getPassword('publii-netlify-token', account); } client = new NetlifyAPI({ accessToken: (token).toString().trim(), siteID: (siteID).toString().trim(), inputDir: '' }); try { await client.testConnection(); waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-success'); } catch (err) { waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-error', { message: stripTags((err.message).toString()) }); } setTimeout(function() { if(waitForTimeout === true) { app.mainWindow.webContents.send('app-deploy-test-error', { message: { translation: 'core.server.requestTimeout' } }); } }, 10000); } } module.exports = Netlify; ================================================ FILE: app/back-end/modules/deploy/s3.js ================================================ /* * Class used to upload files to the S3 bucket */ const fs = require('fs-extra'); const path = require('path'); const { S3Client, ListObjectsCommand, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } = require("@aws-sdk/client-s3"); const passwordSafeStorage = require('keytar'); const slug = require('./../../helpers/slug'); const mime = require('mime'); const stripTags = require('striptags'); class S3 { constructor(deploymentInstance = false) { this.deployment = deploymentInstance; this.connection = false; this.econnresetCounter = 0; this.waitForTimeout = false; this.softUploadErrors = {}; this.hardUploadErrors = []; } async initConnection() { let s3Provider = this.deployment.siteConfig.deployment.s3.provider; let s3Endpoint = this.deployment.siteConfig.deployment.s3.endpoint; let s3Id = this.deployment.siteConfig.deployment.s3.id; let s3Key = this.deployment.siteConfig.deployment.s3.key; let region = this.deployment.siteConfig.deployment.s3.region; let customRegion = this.deployment.siteConfig.deployment.s3.customRegion; let account = slug(this.deployment.siteConfig.name); this.bucket = this.deployment.siteConfig.deployment.s3.bucket; this.prefix = this.deployment.siteConfig.deployment.s3.prefix; this.waitForTimeout = true; if (this.deployment.siteConfig.uuid) { account = this.deployment.siteConfig.uuid; } if (s3Id === 'publii-s3-id ' + account) { s3Id = await passwordSafeStorage.getPassword('publii-s3-id', account); } if (s3Key === 'publii-s3-key ' + account) { s3Key = await passwordSafeStorage.getPassword('publii-s3-key', account); } if (s3Provider !== 'aws' && typeof s3Endpoint === 'string' && s3Endpoint.indexOf('://') === -1) { s3Endpoint = 'https://' + s3Endpoint; } let connectionParams; if (s3Provider === 'aws') { connectionParams = { credentials: { accessKeyId: s3Id, secretAccessKey: s3Key, }, region: region } } else { connectionParams = { credentials: { accessKeyId: s3Id, secretAccessKey: s3Key, }, endpoint: s3Endpoint, region: customRegion } } this.connection = new S3Client(connectionParams); this.sendProgress(6, false); process.send({ type: 'web-contents', message: 'app-connection-in-progress' }); let params = { Bucket: this.bucket, Prefix: this.prefix, MaxKeys: 1 }; try { await this.connection.send(new ListObjectsCommand(params)); this.waitForTimeout = false; process.send({ type: 'web-contents', message: 'app-connection-success' }); this.deployment.setInput(); this.deployment.setOutput(true); this.deployment.prepareLocalFilesList(); this.sendProgress(7, false); await this.downloadFilesList(); } catch (err) { this.onError(err); } setTimeout(() => { if(this.waitForTimeout === true) { process.send({ type: 'web-contents', message: 'app-connection-error' }); setTimeout(() => { process.kill(process.pid, 'SIGTERM'); }, 1000); } }, 20000); } async downloadFilesList() { let fileName = 'files.publii.json'; if (typeof this.prefix === 'string' && this.prefix !== '') { fileName = this.prefix + fileName; } let params = { Bucket: this.bucket, Key: fileName, }; try { let data = await this.connection.send(new GetObjectCommand(params)); console.log(`[${new Date().toUTCString()}] <- files.publii.json`); this.sendProgress(8, false); let remoteFile = await this.s3streamToString(data.Body); this.deployment.checkLocalListWithRemoteList(remoteFile); } catch (err) { console.log(`[${new Date().toUTCString()}] <- files.publii.json`); if (err.name !== 'NoSuchKey') { this.onError(err); return; } this.sendProgress(8, false); this.deployment.compareFilesList(false); } } async uploadNewFileList() { this.sendProgress(99); let fileName = 'files.publii.json'; if (typeof this.prefix === 'string' && this.prefix !== '') { fileName = this.prefix + fileName; } let filePath = path.join(this.deployment.inputDir, 'files.publii.json'); fs.readFile(filePath, async (err, fileContent) => { if (err) { this.onError(err); return; } let fileACL = this.deployment.siteConfig.deployment.s3.acl || 'public-read'; let params = { ACL: fileACL, Body: fileContent, Bucket: this.bucket, Key: fileName, ContentType: mime.getType(fileName) || 'application/json' }; try { await this.connection.send(new PutObjectCommand(params)); console.log(`[${new Date().toUTCString()}] -> ${fileName}`); this.sendProgress(100, false); process.send({ type: 'sender', message: 'app-deploy-uploaded', value: { status: true, issues: this.hardUploadErrors.length > 0 } }); setTimeout(() => { process.kill(process.pid, 'SIGTERM'); }, 1000); } catch (uploadErr) { console.log(`[${new Date().toUTCString()}] -> ${fileName}`); this.onError(uploadErr); } }); } /** * Uploads file */ async uploadFile() { if (this.deployment.filesToUpload.length > 0) { let fileToUpload = this.deployment.filesToUpload.pop(); fileToUpload.path = this.prepareFilePath(fileToUpload.path); if (fileToUpload.type === 'file') { await this.uploadFileObject(fileToUpload.path); } else { await this.uploadFile(); } } else { this.sendProgress(98); await this.uploadNewFileList(); } } async uploadFileObject(input) { let filePath = path.join(this.deployment.inputDir, input); fs.readFile(filePath, async (err, fileContent) => { if (err) { this.onError(err); return; } let fileName = input; if (typeof this.prefix === 'string' && this.prefix !== '') { fileName = this.prefix + fileName; } let fileACL = this.deployment.siteConfig.deployment.s3.acl || 'public-read'; let htmlCacheControl = this.deployment.siteConfig.deployment.s3.htmlCacheControl || 'no-cache, no-store'; let otherCacheControl = this.deployment.siteConfig.deployment.s3.otherCacheControl || 'public, max-age=2592000'; let fileExtension = path.extname(fileName).substring(1); let cacheControl = fileExtension === 'html' ? htmlCacheControl : otherCacheControl; let params = { ACL: fileACL, Body: fileContent, Bucket: this.bucket, Key: fileName, CacheControl: cacheControl, ContentType: mime.getType(fileExtension) || 'application/octet-stream' }; try { await this.connection.send(new PutObjectCommand(params)); this.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${fileName}`); this.deployment.progressOfUploading += this.deployment.progressPerFile; this.sendProgress(8 + Math.floor(this.deployment.progressOfUploading)); await this.uploadFile(); } catch (uploadErr) { this.onError(uploadErr, true); setTimeout(async () => { if (!this.softUploadErrors[input]) { this.softUploadErrors[input] = 1; } else { this.softUploadErrors[input]++; } if (this.softUploadErrors[input] <= 5) { await this.uploadFileObject(input); } else { this.hardUploadErrors.push(input); this.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] UPL HARD ERR ${input} -> ${fileName}`); this.deployment.progressOfUploading += this.deployment.progressPerFile; this.sendProgress(8 + Math.floor(this.deployment.progressOfUploading)); await this.uploadFile(); } }, 500); } }); } async removeFile() { if (this.deployment.filesToRemove.length > 0) { let fileToRemove = this.deployment.filesToRemove.pop(); fileToRemove.path = this.prepareFilePath(fileToRemove.path); if(fileToRemove.type === 'file') { await this.removeFileObject(fileToRemove.path); } else { await this.removeFile(); } } else { this.sendProgress(8 + Math.floor(this.deployment.progressOfUploading)); await this.uploadFile(); } } async removeFileObject(input) { let params = { Bucket: this.bucket, Key: input }; try { await this.connection.send(new DeleteObjectCommand(params)); this.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] DEL ${input}`); this.deployment.progressOfDeleting += this.deployment.progressPerFile; this.sendProgress(8 + Math.floor(this.deployment.progressOfDeleting)); await this.removeFile(); } catch (err) { // Handle case when specific file no longer exists in the bucket - don't block sync if (err.name === 'NoSuchKey') { this.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] DEL ${input} - NoSuchKey`); this.deployment.progressOfDeleting += this.deployment.progressPerFile; this.sendProgress(8 + Math.floor(this.deployment.progressOfDeleting)); await this.removeFile(); return; } console.error(`[${new Date().toUTCString()}] Error deleting ${input}`, err); this.onError(err, true); } } onError(err, silentMode = false) { console.log(`[${ new Date().toUTCString() }] S3 ERROR: ${err.message}`); if(this.waitForTimeout && !silentMode) { this.waitForTimeout = false; process.send({ type: 'web-contents', message: 'app-connection-error' }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); } } prepareFilePath(filePath) { if (filePath[0] && filePath[0] === '/') { filePath = filePath.substr(1); } return filePath; } async testConnection(app, deploymentConfig, siteName, uuid) { let s3Provider = deploymentConfig.s3.provider; let s3Endpoint = deploymentConfig.s3.endpoint; let s3Id = deploymentConfig.s3.id; let s3Key = deploymentConfig.s3.key; let bucket = deploymentConfig.s3.bucket; let prefix = deploymentConfig.s3.prefix; let region = deploymentConfig.s3.region; let customRegion = deploymentConfig.s3.customRegion; let account = slug(siteName); let waitForTimeout = true; if (uuid) { account = uuid; } if (s3Id === 'publii-s3-id ' + account) { s3Id = await passwordSafeStorage.getPassword('publii-s3-id', account); } if (s3Key === 'publii-s3-key ' + account) { s3Key = await passwordSafeStorage.getPassword('publii-s3-key', account); } let connectionParams; if (s3Provider === 'aws') { connectionParams = { credentials: { accessKeyId: s3Id, secretAccessKey: s3Key, }, region: region } } else { connectionParams = { credentials: { accessKeyId: s3Id, secretAccessKey: s3Key, }, endpoint: s3Endpoint, region: customRegion } } this.connection = new S3Client(connectionParams); let testParams = { Bucket: bucket, Prefix: prefix, MaxKeys: 1 }; try { await this.connection.send(new ListObjectsCommand(testParams)); } catch (err) { waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-error', { message: stripTags((err.message).toString()) }); return; } waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-success'); setTimeout(function() { if (waitForTimeout === true) { app.mainWindow.webContents.send('app-deploy-test-error', { message: { translation: 'core.server.requestTimeout' } }); } }, 10000); } sendProgress (progress, showOperations = true) { let operations = [this.deployment.currentOperationNumber, this.deployment.operationsCounter]; if (!showOperations) { operations = false; } process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress, operations } }); } async s3streamToString (stream) { let chunks = []; for await (let chunk of stream) { chunks.push(chunk); } return Buffer.concat(chunks).toString('utf-8'); } } module.exports = S3; ================================================ FILE: app/back-end/modules/deploy/sftp.js ================================================ /* * Class used to upload files to the SFTP server */ const fs = require('fs-extra'); const path = require('path'); const FileHelper = require('./../../helpers/file.js'); const sftpClient = require('ssh2-sftp-client'); const passwordSafeStorage = require('keytar'); const slug = require('./../../helpers/slug'); const normalizePath = require('normalize-path'); class SFTP { constructor(deploymentInstance = false) { this.deployment = deploymentInstance; this.connection = false; } async initConnection() { let waitForTimeout = true; let ftpPassword = this.deployment.siteConfig.deployment.password; let passphrase = this.deployment.siteConfig.deployment.passphrase; let account = slug(this.deployment.siteConfig.name); this.connection = new sftpClient(); if (this.deployment.siteConfig.uuid) { account = this.deployment.siteConfig.uuid; } if(ftpPassword === 'publii ' + account) { ftpPassword = await passwordSafeStorage.getPassword('publii', account); } if(passphrase === 'publii-passphrase ' + account) { passphrase = await passwordSafeStorage.getPassword('publii-passphrase', account); } let connectionSettings = { host: this.deployment.siteConfig.deployment.server, port: this.deployment.siteConfig.deployment.port, username: this.deployment.siteConfig.deployment.username }; if(this.deployment.siteConfig.deployment.protocol === 'sftp') { connectionSettings.password = ftpPassword; } else { let keyPath = this.deployment.siteConfig.deployment.sftpkey; if(passphrase !== '') { connectionSettings.passphrase = passphrase; } connectionSettings.privateKey = FileHelper.readFileSync(keyPath); } this.connection.connect(connectionSettings).then(() => { process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 6, operations: false } }); process.send({ type: 'web-contents', message: 'app-connection-in-progress' }); waitForTimeout = false; process.send({ type: 'web-contents', message: 'app-connection-success' }); this.deployment.setInput(); this.deployment.setOutput(); this.deployment.prepareLocalFilesList(); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 7, operations: false } }); this.downloadFilesList(); }).catch(err => { console.log(`[${ new Date().toUTCString() }] ERR (1): ${err}`); this.connection.end(); process.send({ type: 'web-contents', message: 'app-connection-error' }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); }); setTimeout(() => { if(waitForTimeout === true) { this.connection.end(); process.send({ type: 'web-contents', message: 'app-connection-error' }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); } }, 20000); } downloadFilesList() { this.connection.get( normalizePath(path.join(this.deployment.outputDir, 'files.publii.json')) ).then((stream) => { console.log(`[${ new Date().toUTCString() }] <- files.publii.json`); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8, operations: false } }); this.deployment.checkLocalListWithRemoteList(stream); }).catch(err => { console.log(`[${ new Date().toUTCString() }] ERR (2): ${err} (${err.stack}) [<- files.publii.json]`); try { this.deployment.compareFilesList(false); } catch (err) { console.log(`[${ new Date().toUTCString() }] ERR (3): ${err} (${err.stack}) [<- files.publii.json]`); } }); } uploadNewFileList() { let self = this; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 99, operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter] } }); this.connection.put( normalizePath(path.join(self.deployment.inputDir, 'files.publii.json')), normalizePath(path.join(self.deployment.outputDir, 'files.publii.json')), ).then(() => { this.connection.chmod(normalizePath(path.join(this.deployment.outputDir, 'files.publii.json')), 0o644).then(() => { console.log(`[${ new Date().toUTCString() }] -> files.publii.json`); this.connection.end(); process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 100, operations: false } }); process.send({ type: 'sender', message: 'app-deploy-uploaded', value: { status: true } }); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); }).catch(err => { this.connection.end(); console.log(`[${ new Date().toUTCString() }] ${err}`); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); }); }).catch(err => { this.connection.end(); console.log(`[${ new Date().toUTCString() }] ${err}`); setTimeout(function () { process.kill(process.pid, 'SIGTERM'); }, 1000); }); } uploadFile(input, output) { this.connection.put(input, output).then(() => { this.connection.chmod(output, 0o644).then(() => { this.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${output}`); this.deployment.progressOfUploading += this.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(this.deployment.progressOfUploading), operations: [ this.deployment.currentOperationNumber, this.deployment.operationsCounter ] } }); this.deployment.uploadFile(); }).catch(err => { console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD FILE: ${normalizePath(input)}`); console.log(`[${ new Date().toUTCString() }] ${err}`); this.deployment.uploadFile(); }); }).catch(err => { console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD FILE: ${normalizePath(input)}`); console.log(`[${ new Date().toUTCString() }] ${err}`); this.deployment.uploadFile(); }); } uploadDirectory(input, output) { this.connection.mkdir(output, true).then(() => { this.connection.chmod(output, 0o755).then(() => { this.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${output}`); this.deployment.progressOfUploading += this.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(this.deployment.progressOfUploading), operations: [this.deployment.currentOperationNumber, this.deployment.operationsCounter] } }); this.deployment.uploadFile(); }).catch(err => { this.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD DIR: ${output}`); console.log(`[${ new Date().toUTCString() }] ${err}`); this.deployment.progressOfUploading += this.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(this.deployment.progressOfUploading), operations: [ this.deployment.currentOperationNumber, this.deployment.operationsCounter ] } }); this.deployment.uploadFile(); }); }).catch(err => { this.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD DIR: ${output}`); console.log(`[${ new Date().toUTCString() }] ${err}`); this.deployment.progressOfUploading += this.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(this.deployment.progressOfUploading), operations: [ this.deployment.currentOperationNumber, this.deployment.operationsCounter ] } }); this.deployment.uploadFile(); }); } removeFile(input) { let self = this; this.connection.delete(input).then(function (result) { self.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] DEL ${input}`); self.deployment.progressOfDeleting += self.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(self.deployment.progressOfDeleting), operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter] } }); self.deployment.removeFile(); }).catch(err => { console.log(`[${ new Date().toUTCString() }] ERROR REMOVE FILE: ${input}`); console.log(`[${ new Date().toUTCString() }] ${err}`); self.deployment.removeFile(); }); } removeDirectory(input) { let self = this; this.connection.rmdir(input, true).then(function (result) { self.deployment.currentOperationNumber++; console.log(`[${ new Date().toUTCString() }] DEL ${input}`); self.deployment.progressOfDeleting += self.deployment.progressPerFile; process.send({ type: 'web-contents', message: 'app-uploading-progress', value: { progress: 8 + Math.floor(self.deployment.progressOfDeleting), operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter] } }); self.deployment.removeFile(); }).catch(err => { console.log(`[${ new Date().toUTCString() }] ERROR REMOVE DIR ${input}`); console.log(`[${ new Date().toUTCString() }] ${err}`); self.deployment.removeFile(); }); } async testConnection(app, deploymentConfig, siteName, siteConfig) { let client = new sftpClient(); let waitForTimeout = true; let ftpPassword = deploymentConfig.password; let passphrase = deploymentConfig.passphrase; let account = slug(siteName); if (siteConfig.uuid) { account = siteConfig.uuid; } if(ftpPassword === 'publii ' + account) { ftpPassword = await passwordSafeStorage.getPassword('publii', account); } if(passphrase === 'publii-passphrase ' + account) { passphrase = await passwordSafeStorage.getPassword('publii-passphrase', account); } let connectionSettings = { host: deploymentConfig.server, port: deploymentConfig.port, username: deploymentConfig.username }; if(deploymentConfig.protocol === 'sftp') { connectionSettings.password = ftpPassword; } else { let keyPath = deploymentConfig.sftpkey; if(passphrase !== '') { connectionSettings.passphrase = passphrase; } connectionSettings.privateKey = FileHelper.readFileSync(keyPath); } let testFilePath = normalizePath(path.join(app.sitesDir, siteName, 'input', 'publii.test')); client.connect(connectionSettings).then(() => { return client.list('/'); }).then(data => { fs.writeFileSync(testFilePath, 'It is a test file. You can remove it.'); client.put( testFilePath, normalizePath(path.join(deploymentConfig.path, 'publii.test')) ).then(() => { client.chmod(normalizePath(path.join(deploymentConfig.path, 'publii.test')), 0o644).then(() => { client.delete( normalizePath(path.join(deploymentConfig.path, 'publii.test')) ).then(() => { app.mainWindow.webContents.send('app-deploy-test-success'); if (fs.existsSync(testFilePath)) { fs.unlinkSync(testFilePath); } client.end().catch(err => console.log('SFTP session end error')); }).catch(() => { app.mainWindow.webContents.send('app-deploy-test-write-error'); if (fs.existsSync(testFilePath)) { fs.unlinkSync(testFilePath); } client.end().catch(err => console.log('SFTP session end error')); }); }).catch(err => { app.mainWindow.webContents.send('app-deploy-test-write-error'); if (fs.existsSync(testFilePath)) { fs.unlinkSync(testFilePath); } client.end().catch(err => console.log('SFTP session end error')); }); }).catch(err => { app.mainWindow.webContents.send('app-deploy-test-write-error'); if (fs.existsSync(testFilePath)) { fs.unlinkSync(testFilePath); } client.end().catch(err => console.log('SFTP session end error')); }); waitForTimeout = false; app.mainWindow.webContents.send('app-deploy-test-success'); }).catch(err => { console.log(`[${ new Date().toUTCString() }] ${err}`); if(waitForTimeout) { waitForTimeout = false; client.end().catch(err => console.log('SFTP session end error')); app.mainWindow.webContents.send('app-deploy-test-error'); } }); setTimeout(function() { if(waitForTimeout === true) { client.end().catch(err => console.log('SFTP session end error')); app.mainWindow.webContents.send('app-deploy-test-error'); } }, 15000); } } module.exports = SFTP; ================================================ FILE: app/back-end/modules/import/automatic-paragraphs.js ================================================ function automaticParagraphs (inputText) { let rules = [ [/
/gmi, '
'], [//g, matchingText => matchingText.replace(/[\r\n]+/g, '')], [/<[^<>]+>/g, matchingText => matchingText.replace(/[\r\n]+/g, ' ')], [/<(pre|script|style)[^>]*>[\s\S]+?<\/\1>/g, matchingText => matchingText.replace(/(\r\n|\n)/g, '')], [/(<(?:address|aside|blockquote|caption|dd|div|dl|dt|figcaption|figure|h1|h2|h3|h4|h5|h6|header|hr|legend|li|ol|p|pre|section|table|tbody|td|tfoot|th|thead|tr|ul)(?: [^>]*)?>)/gi, "\n$1"], [/(<\/(?:address|aside|blockquote|caption|dd|div|dl|dt|figcaption|figure|h1|h2|h3|h4|h5|h6|header|hr|legend|li|ol|p|pre|section|table|tbody|td|tfoot|th|thead|tr|ul)>)/gi, "$1\n\n"], [/([\s\S]+?)\n\n/g, "

$1

\n"], [/

\s*(<\/?(?:address|aside|blockquote|caption|dd|div|dl|dt|figcaption|figure|h1|h2|h3|h4|h5|h6|header|hr|legend|li|ol|p|pre|section|table|tbody|td|tfoot|th|thead|tr|ul)(?: [^>]*)?>)\s*<\/p>/gi, '$1'], [/

(/gi, '$1'], [/

\s*(<\/?(?:address|aside|blockquote|caption|dd|div|dl|dt|figcaption|figure|h1|h2|h3|h4|h5|h6|header|hr|legend|li|ol|p|pre|section|table|tbody|td|tfoot|th|thead|tr|ul)(?: [^>]*)?>)/gi, '$1'], [/(<\/?(?:address|aside|blockquote|caption|dd|div|dl|dt|figcaption|figure|h1|h2|h3|h4|h5|h6|header|hr|legend|li|ol|p|pre|section|table|tbody|td|tfoot|th|thead|tr|ul)(?: [^>]*)?>)\s*<\/p>/gi, '$1'], [/(<\/?(?:address|aside|blockquote|caption|dd|div|dl|dt|figcaption|figure|h1|h2|h3|h4|h5|h6|header|hr|legend|li|ol|p|pre|section|table|tbody|td|tfoot|th|thead|tr|ul)[^>]*>)\s*
/gi, '$1'], [/
(\s*<\/?(?:dd|div|dl|dt|li|ol|p|pre|td|th|ul)>)/gi, '$1'], [/(<(?:dd|div|th|td)[^>]*>)(.*?)<\/p>/g, (match, offset, content) => (content.match(/]*)?>/)) ? match : offset + '

' + content + '

'], [//g, "\n"] [/

<\/p>/gi, ''] ]; inputText = inputText + "\n\n"; for (let i = 0; i < rules.length; i++) { if (!rules[i]) { continue; } inputText = inputText.replace(rules[i][0], rules[i][1]); } return inputText; } module.exports = automaticParagraphs; ================================================ FILE: app/back-end/modules/import/import.js ================================================ /* * Class used to import data from WP to Publii using WXR file */ const fs = require('fs-extra'); const path = require('path'); const os = require('os'); const WxrParser = require('./wxr-parser'); const Database = os.platform() === 'linux' ? require('node-sqlite3-wasm').Database : require('better-sqlite3'); const DBUtils = require('../../helpers/db.utils.js'); class Import { /** * Creates an instance * * @param appInstance * @param siteName * @param filePath */ constructor(appInstance, siteName, filePath) { this.appInstance = appInstance; this.siteName = siteName; this.filePath = filePath; this.connectWithDB(); this.parser = new WxrParser(appInstance, siteName); this.parser.loadFile(this.filePath); } /** * Creates DB instance for the importer */ connectWithDB() { if(!this.appInstance) { return; } const dbPath = path.join(this.appInstance.sitesDir, this.siteName, 'input', 'db.sqlite'); if (this.appInstance.db) { try { this.appInstance.db.close(); } catch (e) { console.log('[WP IMPORT] DB already closed'); } } this.appInstance.db = new DBUtils(new Database(dbPath)); } /** * Checks the file * * @returns {*} */ checkFile() { if (this.parser.isWXR()) { try { let result = this.parser.getWxrStats(); if (result) { return { status: 'success', message: result }; } return { status: 'error', message: 'An error occurred during parsing selected WXR file' }; } catch (e) { return { status: 'error', message: 'An error occurred during parsing selected WXR file' }; } } return { status: 'error', message: 'Selected file is not a proper WXR file.' }; } /** * Imports data from the given WXR file * * @param importAuthors * @param usedTaxonomy * @returns {{status: string, message: boolean}} */ importFile(importAuthors, usedTaxonomy, autop, postTypes) { console.log('(i) Import started'); this.parser.setConfig(importAuthors, usedTaxonomy, autop, postTypes); this.parser.importAuthorsData(); this.parser.importTagsData(); this.parser.getImageURLs(); this.parser.importPostsData(); this.parser.importPagesData(); this.parser.importImages(); } } module.exports = Import; ================================================ FILE: app/back-end/modules/import/wxr-parser.js ================================================ const fs = require('fs'); const url = require('url'); const path = require('path'); const FileHelper = require('./../../helpers/file.js'); const moment = require('moment'); const { XMLParser } = require('fast-xml-parser'); const download = require('image-downloader'); const automaticParagraphs = require('./automatic-paragraphs.js'); const slug = require('./../../helpers/slug'); const Author = require('./../../author.js'); const Tag = require('./../../tag.js'); const Post = require('./../../post.js'); const Page = require('./../../page.js'); const Utils = require('./../../helpers/utils.js'); /** * Class used to parse WXR files */ class WxrParser { /** * Create an instance * * @param appInstance * @param siteName */ constructor(appInstance, siteName) { this.appInstance = appInstance; this.siteName = siteName; this.importAuthors = false; this.autop = false; this.usedTaxonomy = 'tags'; this.postTypes = []; this.temp = { authors: [], posts: [], pages: [], tags: [], images: [], mapping: { authors: [], tags: [], images: [], posts: [], pages: [] }, imagesQueue: {} }; } /** * Load WXR file and parse it * * @param filePath */ loadFile(filePath) { this.filePath = filePath; this.fileContent = FileHelper.readFileSync(this.filePath, 'utf8'); this.fileContent = this.fileContent.trim(); this.parseFile(); } /** * Check if loaded WXR file is a WXR file * * @returns {boolean} */ isWXR() { if(path.parse(this.filePath).ext !== '.xml') { return false; } if( this.fileContent.indexOf(' with Publii separator text = text.replace(//g, '


'); if(this.autop) { console.log('(i) Used automatic paragraphs for the post content'); text = automaticParagraphs(text); } return text; } /** * Finishing import process */ finishImport() { process.send({ type: 'result', status: 'success', message: true }); console.log('(i) Import is done'); setTimeout(function() { process.exit(); }, 1000); } } module.exports = WxrParser; ================================================ FILE: app/back-end/modules/plugins/plugins-api.js ================================================ class PluginsAPI { constructor () { this.events = { app: {}, site: {} }; } /** * Add */ _add (scope, place, callback, priority) { if (!this.events[scope][place]) { this.events[scope][place] = [{ priority, callback }]; } else { this.events[scope][place].push({ priority, callback }); } this.events[scope][place].sort(this.sortByPriority); } addSiteEvent (event, callback, priority) { this._add('site', event, callback, priority); } addAppEvent (event, callback, priority) { this._add('app', event, callback, priority); } /** * Get */ _get (scope, place) { if (!this.events[scope][place]) { return []; } return this.events[scope][place]; } getSiteEvents (event) { return this._get('site', event); } getAppEvents (event) { return this._get('app', event); } /** * Remove */ _remove (scope, place, callback, priority) { if (!this.events[scope][place]) { return; } this.events[scope][place] = this.events[scope][place].filter(insertion => insertion.callback !== callback && insertion.priority !== priority); } removeSiteEvent (event, callback, priority) { this._remove('site', event, callback, priority); } removeAppEvent (event, callback, priority) { this._remove('app', event, callback, priority); } /** * Reset */ _reset (scope) { this.events[scope] = {} } resetSiteEvents () { this._reset('site'); } resetAppEvents () { this._reset('app'); } /** * Helpers */ sortByPriority (itemA, itemB) { return itemA.priority - itemB.priority; } } module.exports = PluginsAPI; ================================================ FILE: app/back-end/modules/plugins/plugins-helpers.js ================================================ const path = require('path'); const FileHelper = require('./../../helpers/file.js'); class PluginsHelpers { // Returns a list of active plugins for given site plugins config file; static getActivePluginsList (sitePluginsConfigPath) { let fileContent; let allPlugins; let activePlugins = []; try { fileContent = FileHelper.readFileSync(sitePluginsConfigPath); fileContent = fileContent.toString(); fileContent = JSON.parse(fileContent); } catch (e) { console.log('(!) Unable to find site plugins config JSON'); return []; } allPlugins = Object.keys(fileContent); for (let i = 0; i < allPlugins.length; i++) { let pluginName = allPlugins[i]; if (fileContent[pluginName]) { activePlugins.push(pluginName); } } return activePlugins; } // Returns a list of files which should be copied to the website static getPluginFrontEndFiles (pluginName, pluginsDir) { let pluginConfigPath = path.join(pluginsDir, pluginName, 'plugin.json'); let pluginConfig; try { pluginConfig = FileHelper.readFileSync(pluginConfigPath); pluginConfig = pluginConfig.toString(); pluginConfig = JSON.parse(pluginConfig); } catch (e) { console.log('(!) Unable to read plugin config file (plugin.json): ' + pluginName); return []; } if (pluginConfig.assets && pluginConfig.assets.front) { pluginConfig.assets.front = pluginConfig.assets.front.map(fileName => fileName.split('/')); return pluginConfig.assets.front.map(fileName => ({ input: path.join(pluginsDir, pluginName, 'front-assets', ...fileName), output: fileName.join('/') })); } return []; } } module.exports = PluginsHelpers; ================================================ FILE: app/back-end/modules/render-html/contexts/404.js ================================================ // Necessary packages const RendererContext = require('../renderer-context.js'); /** * Class used create context * for the homepage theme view */ class RendererContext404 extends RendererContext { /** * Loading data used in the view */ loadData() { let siteName = this.siteConfig.name; if(this.siteConfig.displayName) { siteName = this.siteConfig.displayName; } this.tags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true); this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true); this.menus = this.renderer.commonData.menus; this.unassignedMenus = this.renderer.commonData.unassignedMenus; this.authors = this.renderer.commonData.authors; this.featuredPosts = this.renderer.commonData.featuredPosts.homepage; this.hiddenPosts = this.renderer.commonData.hiddenPosts; this.pages = this.renderer.commonData.pages; this.metaTitle = this.siteConfig.advanced.errorMetaTitle.replace(/%sitename/g, siteName); this.metaDescription = this.siteConfig.advanced.errorMetaDescription.replace(/%sitename/g, siteName); if (this.metaTitle === '') { this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName); } if (this.metaDescription === '') { this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName); } // mark tags as main tags let mainTagsIds = this.mainTags.map(tag => tag.id); this.tags = this.tags.map(tag => { tag.isMainTag = mainTagsIds.includes(tag.id); return tag; }); } /** * Preparing the loaded data */ prepareData() { this.title = this.siteConfig.name; this.featuredPosts = this.featuredPosts || []; this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.hiddenPosts = this.hiddenPosts || []; this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.pages = this.pages || []; this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]); } /** * Setting context for the view */ setContext() { this.loadData(); this.prepareData(); let metaRobotsValue = this.siteConfig.advanced.metaRobotsError; if(this.siteConfig.advanced.noIndexThisPage) { metaRobotsValue = 'noindex,nofollow'; } this.context = { title: this.metaTitle !== '' ? this.metaTitle : this.title, featuredPosts: this.featuredPosts, hiddenPosts: this.hiddenPosts, tags: this.tags, pages: this.pages, mainTags: this.mainTags, authors: this.authors, metaTitleRaw: this.metaTitle, metaDescriptionRaw: this.metaDescription, metaRobotsRaw: metaRobotsValue, siteOwner: this.renderer.cachedItems.authors[1], menus: this.menus, unassignedMenus: this.unassignedMenus }; } /** * Getting context for the view * * @returns {object} - context for the view */ getContext() { this.setContext(); return this.context; } } module.exports = RendererContext404; ================================================ FILE: app/back-end/modules/render-html/contexts/author.js ================================================ // Necessary packages const RendererContext = require('../renderer-context'); const slug = require('./../../../helpers/slug'); const RendererHelpers = require('./../helpers/helpers.js'); const path = require('path'); /** * Class used create context * for the author theme views */ class RendererContextAuthor extends RendererContext { loadData() { // Prepare query data this.authorID = parseInt(this.authorID, 10); this.postsNumber = parseInt(this.postsNumber, 10); this.offset = parseInt(this.offset, 10); // Retrieve author data this.author = this.renderer.cachedItems.authors[this.authorID]; // Retrieve post let includeFeaturedPosts = ''; let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('authorsIncludeFeaturedInPosts', this.themeConfig) === false; if (shouldSkipFeaturedPosts) { includeFeaturedPosts = 'status NOT LIKE \'%featured%\' AND'; } if(this.postsNumber === -1) { this.postsNumber = 999; } if(this.postsNumber === 0) { this.posts = false; } else { this.posts = this.db.prepare(` SELECT id FROM posts WHERE status LIKE '%published%' AND status NOT LIKE '%hidden%' AND status NOT LIKE '%trashed%' AND status NOT LIKE '%is-page%' AND ${includeFeaturedPosts} authors LIKE @authorID ORDER BY ${this.postsOrdering} LIMIT @postsNumber OFFSET @offset `).all({ authorID: this.authorID.toString(), postsNumber: this.postsNumber, offset: this.offset }); } this.tags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true); this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true); this.menus = this.renderer.commonData.menus; this.unassignedMenus = this.renderer.commonData.unassignedMenus; this.authors = this.renderer.commonData.authors; this.featuredPosts = this.renderer.commonData.featuredPosts.author; this.hiddenPosts = this.renderer.commonData.hiddenPosts; this.pages = this.renderer.commonData.pages; // mark tags as main tags let mainTagsIds = this.mainTags.map(tag => tag.id); this.tags = this.tags.map(tag => { tag.isMainTag = mainTagsIds.includes(tag.id); return tag; }); } prepareData() { this.title = 'Author: ' + this.author.name; this.posts = this.posts || []; this.posts = this.posts.map(post => this.renderer.cachedItems.posts[post.id]); this.featuredPosts = this.featuredPosts || []; this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.hiddenPosts = this.hiddenPosts || []; this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.pages = this.pages || []; this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]); let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('authorsIncludeFeaturedInPosts', this.themeConfig) === false; let featuredPostsNumber = RendererHelpers.getRendererOptionValue('authorsFeaturedPostsNumber', this.themeConfig); // Remove featured posts from posts if featured posts allowed if (shouldSkipFeaturedPosts && (featuredPostsNumber > 0 || featuredPostsNumber === -1)) { let featuredPostsIds = this.featuredPosts.map(post => post.id); this.posts = this.posts.filter(post => featuredPostsIds.indexOf(post.id) === -1); } // Prepare meta data let siteName = this.siteConfig.name; if(this.siteConfig.displayName) { siteName = this.siteConfig.displayName; } this.metaTitle = this.siteConfig.advanced.authorMetaTitle.replace(/%authorname/g, this.author.name) .replace(/%sitename/g, siteName); this.metaDescription = this.siteConfig.advanced.authorMetaDescription.replace(/%authorname/g, this.author.name) .replace(/%sitename/g, siteName); this.metaRobots = false; this.hasCustomCanonicalUrl = false; this.canonicalUrl = ''; let metaData = this.author.config; let additionalData = this.author.additionalData; if (metaData && metaData.metaTitle) { this.metaTitle = metaData.metaTitle.replace(/%authorname/g, this.author.name) .replace(/%sitename/g, siteName); } if (metaData && metaData.metaDescription) { this.metaDescription = metaData.metaDescription.replace(/%authorname/g, this.author.name) .replace(/%sitename/g, siteName); } if (this.metaTitle === '') { this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName); } if (this.metaDescription === '') { this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName); } if (additionalData && additionalData.metaRobots) { this.metaRobots = additionalData.metaRobots; } if (additionalData && additionalData.canonicalUrl) { this.canonicalUrl = additionalData.canonicalUrl; this.hasCustomCanonicalUrl = true; this.metaRobots = ''; } } setContext() { this.loadData(); this.prepareData(); let metaRobotsValue = this.siteConfig.advanced.metaRobotsAuthors; if (this.metaRobots !== false) { metaRobotsValue = this.metaRobots; } if (this.siteConfig.advanced.noIndexThisPage) { metaRobotsValue = 'noindex,nofollow'; } this.context = { title: this.metaTitle !== '' ? this.metaTitle : this.title, author: this.author, posts: this.posts, pages: this.pages, featuredPosts: this.featuredPosts, hiddenPosts: this.hiddenPosts, tags: this.tags, mainTags: this.mainTags, authors: this.authors, metaTitleRaw: this.metaTitle, metaDescriptionRaw: this.metaDescription, metaRobotsRaw: metaRobotsValue, hasCustomCanonicalUrl: this.hasCustomCanonicalUrl, canonicalUrl: this.canonicalUrl, siteOwner: this.renderer.cachedItems.authors[1], menus: this.menus, unassignedMenus: this.unassignedMenus }; } getContext(authorID, offset = 0, postsNumber = 999) { this.offset = offset; this.postsNumber = postsNumber; this.authorID = authorID; this.setContext(); return this.context; } } module.exports = RendererContextAuthor; ================================================ FILE: app/back-end/modules/render-html/contexts/feed.js ================================================ // Necessary packages const RendererContext = require('../renderer-context.js'); const URLHelper = require('./../helpers/url.js'); const ContentHelper = require('./../helpers/content.js'); const normalizePath = require('normalize-path'); /** * Class used create context * for the feed theme view */ class RendererContextFeed extends RendererContext { loadData() { // prepare query variables this.postsNumber = parseInt(this.postsNumber, 10); this.offset = parseInt(this.offset, 10); // Handle "only featured posts" mode let featuredPostsCondition = ''; if (this.siteConfig.advanced.feed.showOnlyFeatured) { featuredPostsCondition = 'status LIKE \'%featured%\' AND'; } else if (this.siteConfig.advanced.feed.excludeFeatured) { featuredPostsCondition = 'status NOT LIKE \'%featured%\' AND'; } // Retrieve post this.posts = this.db.prepare(` SELECT * FROM posts WHERE status LIKE '%published%' AND ${featuredPostsCondition} status NOT LIKE '%hidden%' AND status NOT LIKE '%is-page%' AND status NOT LIKE '%trashed%' AND status NOT LIKE '%excluded_homepage%' ORDER BY created_at DESC LIMIT @postsNumber OFFSET @offset `).all({ postsNumber: this.postsNumber, offset: this.offset }); } prepareData() { let self = this; this.posts = this.posts || []; this.posts = this.posts.map(post => this.renderer.cachedItems.posts[post.id]); this.posts = this.posts.map(post => { let contentMode = self.siteConfig.advanced.feed.showFullText ? 'fullText' : 'excerpt'; return { title: post.title, url: post.url, author: this.getAuthor('post', post.id), text: contentMode === 'fullText' ? post.text : false, excerpt: post.excerpt, createdAt: post.createdAt, modifiedAt: post.createdAt > post.modifiedAt ? post.createdAt : post.modifiedAt, // Get higher date - created_at or modified_at categories: this.getPostCategories(post.id), thumbnail: this.getPostThumbnail(post.id) } }); } setContext() { this.loadData(); this.prepareData(); let siteOwnerData = this.renderer.cachedItems.authors[1]; let logoUrl = normalizePath(this.themeConfig.config.logo); let siteName = this.siteConfig.name; if (this.siteConfig.advanced.feed.title === 'customTitle') { siteName = this.siteConfig.advanced.feed.titleValue; } else { if (this.siteConfig.displayName) { siteName = this.siteConfig.displayName; } } if(logoUrl !== '') { logoUrl = normalizePath(this.siteConfig.domain) + '/' + normalizePath(this.themeConfig.config.logo); logoUrl = URLHelper.fixProtocols(logoUrl); } this.context = { siteName: siteName, siteAuthor: siteOwnerData, siteDomain: this.siteConfig.domain, siteLogo: logoUrl, updatedDateType: this.siteConfig.advanced.feed.updatedDateType, siteLastUpdate: this.getLastUpdateDate(), posts: this.posts }; } getContext(postsNumber = 10) { this.offset = 0; this.postsNumber = postsNumber; this.setContext(); return this.context; } getLastUpdateDate() { let latestDate = 0; for(let i = 0; i < this.posts.length; i++) { if(this.posts[i].modifiedAt > latestDate) { latestDate = this.posts[i].modifiedAt; } } return latestDate; } getPostCategories(postID) { let tags = this.db.prepare(` SELECT t.name AS name FROM tags AS t LEFT JOIN posts_tags AS pt ON pt.tag_id = t.id WHERE pt.post_id = @postID AND ( (json_valid(t.additional_data) AND json_extract(t.additional_data, '$.isHidden') = false) OR t.additional_data IS NULL OR t.additional_data = '' ) ORDER BY name DESC `).all({ postID: postID }); return tags; } getPostThumbnail(postID) { if(!this.siteConfig.advanced.feed.showFeaturedImage) { return false; } let thumbnailUrl = ''; let thumbnailAlt = ''; let thumbnail = this.db.prepare(` SELECT pi.url AS url, pi.additional_data AS additionalData FROM posts_images AS pi WHERE pi.post_id = @postID LIMIT 1 `).get({ postID: postID }); if(thumbnail && thumbnail.url) { let additionalData = { alt: '' }; thumbnailUrl = this.siteConfig.domain + '/media/posts/' + postID + '/' + thumbnail.url; try { additionalData = JSON.parse(thumbnail.additionalData); thumbnailAlt = additionalData.alt; } catch (e) { console.log('Malformed thumnail additional data'); } } else { return false; } return { url: thumbnailUrl, alt: thumbnailAlt }; } /** * * Function used to retrieve an author data * * @param dataType (string) - 'post' or 'author' * @param id (int) - ID of post or author * * @return object - author data * */ getAuthor(dataType, id) { let authorID = id; if(dataType === 'post') { let result = this.db.prepare(`SELECT authors FROM posts WHERE id = @id LIMIT 1;`).get({ id: id }); if (result && result.authors) { authorID = parseInt(result.authors, 10); } else { authorID = 1; } } return this.renderer.cachedItems.authors[authorID]; } } module.exports = RendererContextFeed; ================================================ FILE: app/back-end/modules/render-html/contexts/home.js ================================================ // Necessary packages const RendererContext = require('../renderer-context.js'); const RendererHelpers = require('./../helpers/helpers.js'); /** * Class used create context * for the homepage theme view */ class RendererContextHome extends RendererContext { loadData() { // prepare query variables this.postsNumber = parseInt(this.postsNumber, 10); this.offset = parseInt(this.offset, 10); // Retrieve post let includeFeaturedPosts = ''; let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('includeFeaturedInPosts', this.themeConfig) === false; if (shouldSkipFeaturedPosts) { includeFeaturedPosts = 'status NOT LIKE \'%featured%\' AND'; } if(this.postsNumber === -1) { this.postsNumber = 999; } if(this.postsNumber === 0) { this.posts = false; } else { this.posts = this.db.prepare(` SELECT * FROM posts WHERE ${includeFeaturedPosts} status LIKE '%published%' AND status NOT LIKE '%hidden%' AND status NOT LIKE '%trashed%' AND status NOT LIKE '%is-page%' AND status NOT LIKE '%excluded_homepage%' ORDER BY ${this.postsOrdering} LIMIT @postsNumber OFFSET @offset `).all({ postsNumber: this.postsNumber, offset: this.offset }); } let siteName = this.siteConfig.name; if(this.siteConfig.displayName) { siteName = this.siteConfig.displayName; } this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName); this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName); if ( !this.siteConfig.advanced.usePageAsFrontpage && this.siteConfig.advanced.urls.postsPrefix && this.renderer.menuContext.indexOf('frontpage') > -1 ) { this.metaTitle = this.siteConfig.advanced.homepageMetaTitle.replace(/%sitename/g, siteName); this.metaDescription = this.siteConfig.advanced.homepageMetaDescription.replace(/%sitename/g, siteName); } this.tags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true); this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true); this.menus = this.renderer.commonData.menus; this.unassignedMenus = this.renderer.commonData.unassignedMenus; this.authors = this.renderer.commonData.authors; this.pages = this.renderer.commonData.pages; this.featuredPosts = this.renderer.commonData.featuredPosts.homepage; this.hiddenPosts = this.renderer.commonData.hiddenPosts; // mark tags as main tags let mainTagsIds = this.mainTags.map(tag => tag.id); this.tags = this.tags.map(tag => { tag.isMainTag = mainTagsIds.includes(tag.id); return tag; }); } prepareData() { this.title = this.siteConfig.name; this.posts = this.posts || []; this.posts = this.posts.map(post => this.renderer.cachedItems.posts[post.id]); this.featuredPosts = this.featuredPosts || []; this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.hiddenPosts = this.hiddenPosts || []; this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.pages = this.pages || []; this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]); let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('includeFeaturedInPosts', this.themeConfig) == false; let featuredPostsNumber = RendererHelpers.getRendererOptionValue('featuredPostsNumber', this.themeConfig); // Remove featured posts from posts if featured posts not allowed if (shouldSkipFeaturedPosts && (featuredPostsNumber > 0 || featuredPostsNumber === -1)) { let featuredPostsIds = this.featuredPosts.map(post => post.id); this.posts = this.posts.filter(post => featuredPostsIds.indexOf(post.id) === -1); } } setContext() { this.loadData(); this.prepareData(); let metaRobotsValue = this.siteConfig.advanced.metaRobotsIndex; if(this.siteConfig.advanced.noIndexThisPage) { metaRobotsValue = 'noindex,nofollow'; } this.context = { title: this.metaTitle !== '' ? this.metaTitle : this.title, posts: this.posts, featuredPosts: this.featuredPosts, hiddenPosts: this.hiddenPosts, tags: this.tags, mainTags: this.mainTags, pages: this.pages, authors: this.authors, metaTitleRaw: this.metaTitle, metaDescriptionRaw: this.metaDescription, metaRobotsRaw: metaRobotsValue, siteOwner: this.renderer.cachedItems.authors[1], menus: this.menus, unassignedMenus: this.unassignedMenus }; } getContext(offset = 0, postsNumber = 999) { this.offset = offset; this.postsNumber = postsNumber; this.setContext(); return this.context; } getPostsNumber() { let includeFeaturedPosts = ''; let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('includeFeaturedInPosts', this.themeConfig) === false; if (shouldSkipFeaturedPosts) { includeFeaturedPosts = 'AND status NOT LIKE \'%featured%\''; } let results = this.db.prepare(` SELECT COUNT(id) FROM posts WHERE status LIKE '%published%' AND status NOT LIKE '%hidden%' AND status NOT LIKE '%trashed%' AND status NOT LIKE '%is-page%' AND status NOT LIKE '%excluded_homepage%' ${includeFeaturedPosts} GROUP BY id `).all(); if(!results || !results.length) { return 0; } return results.length; } } module.exports = RendererContextHome; ================================================ FILE: app/back-end/modules/render-html/contexts/page-preview.js ================================================ // Necessary packages const fs = require('fs'); const path = require('path'); const sizeOf = require('image-size'); const sqlString = require('sqlstring'); const normalizePath = require('normalize-path'); const slug = require('./../../../helpers/slug'); const RendererContext = require('../renderer-context.js'); const RendererHelpers = require('./../helpers/helpers.js'); const URLHelper = require('../helpers/url.js'); const ContentHelper = require('../helpers/content.js'); /** * Class used create context * for the single page theme previews */ class RendererContextPagePreview extends RendererContext { loadData() { // Prepare data this.pageID = parseInt(this.renderer.postData.postID, 10); this.title = this.renderer.postData.title; this.pageImage = this.renderer.postData.featuredImage; this.editor = this.renderer.postData.additionalData.editor; // Retrieve all tags this.allTags = this.getAllTags(); // Retrieve menu data this.menus = this.getMenus(); } prepareData() { let pageURL = this.siteConfig.domain + '/preview.html'; let preparedText = this.prepareContent(this.renderer.postData.text, this.renderer.postData.id); let hasCustomExcerpt = false; let readmoreMatches = preparedText.match(/\/gmi); if (readmoreMatches && readmoreMatches.length) { hasCustomExcerpt = true; } this.page = { id: this.renderer.postData.id, title: this.renderer.postData.title, slug: this.renderer.postData.slug, author: this.renderer.cachedItems.authors[this.renderer.postData.author], url: pageURL, text: preparedText.replace(/\/gmi, ''), excerpt: ContentHelper.prepareExcerpt(this.themeConfig.config.excerptLength, preparedText), createdAt: this.renderer.postData.creationDate, modifiedAt: this.renderer.postData.modificationDate, status: this.renderer.postData.status, featuredImage: {}, hasGallery: preparedText.indexOf('class="gallery') !== -1, isFeatured: this.renderer.postData.status.indexOf('featured') > -1, isHidden: this.renderer.postData.status.indexOf('hidden') > -1, isExcludedOnHomepage: this.renderer.postData.status.indexOf('excluded_homepage') > -1, hasGallery: preparedText.indexOf('class="gallery') !== -1, template: this.renderer.postData.template, hasCustomExcerpt: hasCustomExcerpt }; if (this.pageImage) { this.page.featuredImage = this.getPageFeaturedImages(this.page.id, true); } this.metaTitle = 'It is an example value for the preview mode'; this.metaDescription = 'It is an example value for the preview mode'; this.metaRobots = 'It is an example value for the preview mode'; } setContext() { this.loadData(); this.prepareData(); let metaRobotsValue = this.metaRobots; if(this.siteConfig.advanced.noIndexThisPage) { metaRobotsValue = 'noindex,nofollow'; } this.context = { title: this.metaTitle !== '' ? this.metaTitle : this.title, page: this.page, tags: this.allTags, metaTitleRaw: this.metaTitle, metaDescriptionRaw: this.metaDescription, metaRobotsRaw: metaRobotsValue, siteOwner: this.renderer.cachedItems.authors[1], menus: this.menus.assigned }; } getContext(pageID) { this.pageID = pageID; this.setContext(); return this.context; } getPageFeaturedImages(pageID, mainPage = false) { let pageImage = false; // Retrieve post image if(mainPage === true) { pageImage = { id: 0, url: this.renderer.postData.featuredImageFilename, additional_data: JSON.stringify(this.renderer.postData.featuredImageData) }; } else { pageImage = this.db.prepare(` SELECT pi.id AS id, pi.url AS url, pi.additional_data AS additional_data FROM posts as p LEFT JOIN posts_images as pi ON p.featured_image_id = pi.id WHERE p.id = @pageID ORDER BY pi.id DESC LIMIT 1 `).get({ pageID: pageID }); } if (pageImage && pageImage.url) { let url = ''; let alt = ''; let caption = ''; let credits = ''; let imageDimensions = false; if (pageImage.additional_data) { let data = JSON.parse(pageImage.additional_data); let pageDirectory = pageID; if(pageDirectory === 0) { pageDirectory = 'temp'; } let imagePath = URLHelper.createImageURL(this.inputDir, pageDirectory, pageImage.url); let domain = this.siteConfig.domain; url = URLHelper.createImageURL(domain, pageDirectory, pageImage.url); alt = data.alt; caption = data.caption; credits = data.credits; try { imageDimensions = sizeOf(imagePath); } catch(e) { console.log('page-preview.js: wrong image path - missing dimensions'); imageDimensions = false; } } else { return false; } let featuredImageSrcSet = false; let featuredImageSizes = false; if(!this.isGifOrSvg(url)) { let useWebp = false; if (this.renderer.siteConfig?.advanced?.forceWebp) { useWebp = true; } featuredImageSrcSet = ContentHelper.getFeaturedImageSrcset(url, this.themeConfig, useWebp); featuredImageSizes = ContentHelper.getFeaturedImageSizes(this.themeConfig); } else { featuredImageSrcSet = ''; featuredImageSizes = ''; } let featuredImageData = { id: pageImage.id, url: url, alt: alt, caption: caption, credits: credits, height: imageDimensions.height, width: imageDimensions.width, srcset: featuredImageSrcSet, sizes: featuredImageSizes }; // Create alternative names for dimensions let dimensions = false; if ( this.themeConfig.files && this.themeConfig.files.responsiveImages ) { if ( this.themeConfig.files.responsiveImages.featuredImages && this.themeConfig.files.responsiveImages.featuredImages.dimensions ) { dimensions = this.themeConfig.files.responsiveImages.featuredImages.dimensions; } else if ( this.themeConfig.files.responsiveImages.contentImages && this.themeConfig.files.responsiveImages.contentImages.dimensions ) { dimensions = this.themeConfig.files.responsiveImages.featuredImages.dimensions; } if (dimensions) { let dimensionNames = Object.keys(dimensions); for (let dimensionName of dimensionNames) { let base = path.parse(url).base; let filename = path.parse(url).name; let extension = path.parse(url).ext; let newFilename = filename + '-' + dimensionName + extension; let capitalizedDimensionName = dimensionName.charAt(0).toUpperCase() + dimensionName.slice(1); if(!this.isGifOrSvg(url)) { featuredImageData['url' + capitalizedDimensionName] = url.replace(base, newFilename); } else { featuredImageData['url' + capitalizedDimensionName] = url; } } } } return featuredImageData; } return false; } prepareContent(originalText, pageID) { let self = this; let domain = normalizePath(self.siteConfig.domain); domain = URLHelper.fixProtocols(domain); // Get media URL let pageDirectory = pageID; if(pageDirectory === 0) { pageDirectory = 'temp'; } let domainMediaPath = domain + '/media/posts/' + pageDirectory + '/'; // Replace domain name constat with real URL to media directory let preparedText = originalText.split('#DOMAIN_NAME#').join(domainMediaPath); preparedText = ContentHelper.parseText(preparedText, this.editor); // Remove TOC plugin ID attributes when TOC does not exist if (preparedText.indexOf('class="post__toc') === -1) { preparedText = preparedText.replace(/\sid="mcetoc_[a-z0-9]*?"/gmi, ''); } // Reduce download="download" to download preparedText = preparedText.replace(/download="download"/gmi, 'download'); // Remove content for AMP or non-AMP depending from ampMode value preparedText = preparedText.replace(/.*<\/publii-amp>/gmi, ''); preparedText = preparedText.replace(//gmi, ''); preparedText = preparedText.replace(/<\/publii-non-amp>/gmi, ''); // Remove read more text preparedText = preparedText.replace(/\/gmi, ''); // Remove the last empty paragraph preparedText = preparedText.replace(/

 <\/p>\s?$/gmi, ''); let useWebp = false; if (this.renderer.siteConfig?.advanced?.forceWebp) { useWebp = true; } // Find all images and add srcset and sizes attributes if (this.siteConfig.responsiveImages) { preparedText = preparedText.replace(/].*?\sloading="[^>].*?>)/gmi, '].*?\sloading="[^>].*?>)/gmi, '].*?\sloading="[^>].*?>)/gmi, '].*?\sloading="[^>].*?>)/gmi, ' preparedText = preparedText.replace(/(\s*?)?]*?(class=".*?").*?>(\s*?<\/p>)?/gmi, function(matches, p1, classes) { return '

' + matches.replace('

', '').replace(//, '').replace(classes, '') + '
'; }); // Fix some specific syntax cases for double figure elements preparedText = preparedText.replace(/
[\s]*?
([\s\S]*?)<\/figure>[\s]*?<\/figure>/gmi, '
$1
'); preparedText = preparedText.replace(/
[\s]*?
([\s\S]*?)<\/figure>[\s]*?
([\s\S]*?)<\/figcaption>[\s]*?<\/figure>/gmi, '
$1
$2
'); } // Remove contenteditable attributes preparedText = preparedText.replace(/contentEditable=".*?"/gi, ''); if (this.editor === 'tinymce') { // Wrap galleries with classes into div with gallery-wrapper CSS class preparedText = preparedText.replace(/
?/gmi, function(matches, classes) { return ''; }); } // Remove paragraphs around '); // Wrap iframes into
preparedText = preparedText.replace(/(?[\s\S]*?)([\s\S]*?<\/iframe>)/gmi, function(matches) { if (matches.indexOf('data-responsive="false"') > -1) { return matches; } return '
' + matches + '
'; }); // Remove CDATA sections inside scripts added by TinyMCE preparedText = preparedText.replace(/\\/\/ \<\!\[CDATA\[/g, ''); return preparedText; } /** * Detects if image is a GIF or SVG */ isGifOrSvg(url) { if(url.slice(-4) === '.gif' || url.slice(-4) === '.svg') { return true; } return false; } } module.exports = RendererContextPagePreview; ================================================ FILE: app/back-end/modules/render-html/contexts/page.js ================================================ // Necessary packages const RendererContext = require('../renderer-context.js'); const stripTags = require('striptags'); /** * Class used create context * for the single page theme views */ class RendererContextPage extends RendererContext { loadData() { // Retrieve meta data let metaDataQuery = this.db.prepare(`SELECT value FROM posts_additional_data WHERE post_id = @pageID AND key = '_core'`); this.metaData = metaDataQuery.get({ pageID: this.pageID}); this.allTags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true); this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true); this.menus = this.renderer.commonData.menus; this.unassignedMenus = this.renderer.commonData.unassignedMenus; this.authors = this.renderer.commonData.authors; this.featuredPosts = this.renderer.commonData.featuredPosts.homepage; this.hiddenPosts = this.renderer.commonData.hiddenPosts; this.pages = this.renderer.commonData.pages; // mark tags as main tags let mainTagsIds = this.mainTags.map(tag => tag.id); this.allTags = this.allTags.map(tag => { tag.isMainTag = mainTagsIds.includes(tag.id); return tag; }); } prepareData() { this.page = this.renderer.cachedItems.pages[this.pageID]; this.featuredPosts = this.featuredPosts || []; this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.hiddenPosts = this.hiddenPosts || []; this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.pages = this.pages || []; this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]); this.metaTitle = this.siteConfig.advanced.pageMetaTitle; this.metaDescription = this.siteConfig.advanced.pageMetaDescription; this.canonicalUrl = this.page.url; this.hasCustomCanonicalUrl = false; this.metaRobots = ''; if (this.siteConfig.advanced.pageMetaDescription === '') { this.metaDescription = stripTags(this.page.excerpt).replace(/\n/gmi, ''); } if(this.metaData && this.metaData.value) { let results = JSON.parse(this.metaData.value); if (results.metaTitle) { this.metaTitle = results.metaTitle; } if (results.metaDesc) { this.metaDescription = results.metaDesc; } if (results.metaRobots) { this.metaRobots = results.metaRobots; } if (results.canonicalUrl) { this.canonicalUrl = results.canonicalUrl; this.hasCustomCanonicalUrl = true; this.metaRobots = ''; } } let siteName = this.siteConfig.name; if(this.siteConfig.displayName) { siteName = this.siteConfig.displayName; } if (this.metaTitle === '') { this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName); } if (this.metaDescription === '') { this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName); } } setContext() { this.loadData(); this.prepareData(); let metaRobotsValue = this.metaRobots; if(this.siteConfig.advanced.noIndexThisPage) { metaRobotsValue = 'noindex,nofollow'; } let siteName = this.siteConfig.name; if (this.siteConfig.displayName) { siteName = this.siteConfig.displayName; } // Detect if the page title is empty if (this.metaTitle === '') { this.metaTitle = this.siteConfig.advanced.pageMetaTitle.replace(/%pagetitle/g, this.page.title) .replace(/%sitename/g, siteName) .replace(/%authorname/g, this.page.author.name); } else { this.metaTitle = this.metaTitle.replace(/%pagetitle/g, this.page.title) .replace(/%sitename/g, siteName) .replace(/%authorname/g, this.page.author.name); } // If still meta title is empty - use page title if (this.metaTitle === '') { this.metaTitle = this.page.title; } this.metaDescription = this.metaDescription.replace(/%pagetitle/g, this.page.title) .replace(/%sitename/g, siteName) .replace(/%authorname/g, this.page.author.name); this.context = { title: this.metaTitle, page: this.page, featuredPosts: this.featuredPosts, hiddenPosts: this.hiddenPosts, tags: this.allTags, mainTags: this.mainTags, authors: this.authors, pages: this.pages, metaTitleRaw: this.metaTitle, metaDescriptionRaw: this.metaDescription, metaRobotsRaw: metaRobotsValue, hasCustomCanonicalUrl: this.hasCustomCanonicalUrl, canonicalUrl: this.canonicalUrl, siteOwner: this.renderer.cachedItems.authors[1], menus: this.menus, unassignedMenus: this.unassignedMenus }; } getContext(pageID) { this.pageID = pageID; this.setContext(); return this.context; } } module.exports = RendererContextPage; ================================================ FILE: app/back-end/modules/render-html/contexts/post-preview.js ================================================ // Necessary packages const fs = require('fs'); const path = require('path'); const sizeOf = require('image-size'); const sqlString = require('sqlstring'); const normalizePath = require('normalize-path'); const slug = require('./../../../helpers/slug'); const RendererContext = require('../renderer-context.js'); const RendererHelpers = require('./../helpers/helpers.js'); const URLHelper = require('../helpers/url.js'); const ContentHelper = require('../helpers/content.js'); /** * Class used create context * for the single post theme previews */ class RendererContextPostPreview extends RendererContext { loadData() { // Prepare data this.postID = parseInt(this.renderer.postData.postID, 10); this.title = this.renderer.postData.title; this.postImage = this.renderer.postData.featuredImage; this.editor = this.renderer.postData.additionalData.editor; // Retrieve post tags if(this.renderer.postData.tags === '') { this.tags = false; } else { this.tags = this.renderer.postData.tags; } // Retrieve all tags this.allTags = this.getAllTags(); // Retrieve menu data this.menus = this.getMenus(); } prepareData() { let postURL = this.siteConfig.domain + '/preview.html'; let preparedText = this.prepareContent(this.renderer.postData.text, this.renderer.postData.id); let hasCustomExcerpt = false; let readmoreMatches = preparedText.match(/\/gmi); if (readmoreMatches && readmoreMatches.length) { hasCustomExcerpt = true; } this.post = { id: this.renderer.postData.id, title: this.renderer.postData.title, slug: this.renderer.postData.slug, author: this.renderer.cachedItems.authors[this.renderer.postData.author], url: postURL, text: preparedText.replace(/\/gmi, ''), excerpt: ContentHelper.prepareExcerpt(this.themeConfig.config.excerptLength, preparedText), createdAt: this.renderer.postData.creationDate, modifiedAt: this.renderer.postData.modificationDate, status: this.renderer.postData.status, featuredImage: {}, hasGallery: preparedText.indexOf('class="gallery') !== -1, isFeatured: this.renderer.postData.status.indexOf('featured') > -1, isHidden: this.renderer.postData.status.indexOf('hidden') > -1, isExcludedOnHomepage: this.renderer.postData.status.indexOf('excluded_homepage') > -1, hasGallery: preparedText.indexOf('class="gallery') !== -1, template: this.renderer.postData.template, hasCustomExcerpt: hasCustomExcerpt }; if(this.postImage) { this.post.featuredImage = this.getPostFeaturedImages(this.post.id, true); } if(this.tags) { this.tags = this.tags.map(tag => { return { id: 0, name: tag, slug: '', description: 'It is an example description for the preview mode', additionalData: '', postsNumber: 0, url: '#' }; }); this.tags.sort((tagA, tagB) => tagA.name.localeCompare(tagB.name)); } this.metaTitle = 'It is an example value for the preview mode'; this.metaDescription = 'It is an example value for the preview mode'; this.metaRobots = 'It is an example value for the preview mode'; this.post.tags = this.tags; // load related posts this.loadRelatedPosts(); // load previous and next posts (only on the visible posts) let renderPrevNextPosts = RendererHelpers.getRendererOptionValue('renderPrevNextPosts', this.themeConfig); if(!renderPrevNextPosts || this.post.status.indexOf('hidden') > -1) { this.nextPost = false; this.previousPost = false; } else { if(this.renderer.postData.id !== 0) { this.loadPost('next', false); } this.loadPost('previous', false); } // load previous and next similar posts (only on the visible posts) let renderSimilarPosts = RendererHelpers.getRendererOptionValue('renderSimilarPosts', this.themeConfig); if(!renderSimilarPosts || this.post.status.indexOf('hidden') > -1) { this.nextSimilarPost = false; this.previousSimilarPost = false; } else { if(this.renderer.postData.id !== 0) { this.loadPost('next', true); } this.loadPost('previous', true); } } loadPost(type, similarPost = false) { let postType = similarPost ? type + 'SimilarPost' : type + 'Post'; let operator = type === 'previous' ? '<=' : '>='; let postData = false; let temporaryPostsOrdering = this.postsOrdering; // For the next posts we have to reverse the results if(type === 'next') { if(temporaryPostsOrdering.indexOf('ASC') > -1) { temporaryPostsOrdering = temporaryPostsOrdering.replace('ASC', 'DESC'); } else { temporaryPostsOrdering = temporaryPostsOrdering.replace('DESC', 'ASC'); } } this.post.createdAt = parseInt(this.post.createdAt, 10); this.post.id = parseInt(this.post.id, 10); let tagsCondition = ''; if(similarPost) { let tags = this.post.tags ? this.post.tags.map(tag => parseInt(tag.id, 10)) : []; if(tags.length) { tagsCondition = ' AND pt.tag_id IN(' + tags.join(',') + ') '; } // Retrieve post postData = this.db.prepare(` SELECT p.id AS id FROM posts AS p LEFT JOIN posts_tags AS pt ON p.id = pt.post_id WHERE p.created_at ${operator} ${this.post.createdAt} AND p.id != @postID AND p.status LIKE '%published%' AND p.status NOT LIKE '%trashed%' AND p.status NOT LIKE '%is-page%' AND p.status NOT LIKE '%hidden%' ${tagsCondition} GROUP BY p.id ORDER BY ${temporaryPostsOrdering} LIMIT 1 `).get({ postID: this.post.id }); } else { // Retrieve post postData = this.db.prepare(` SELECT id FROM posts WHERE created_at ${operator} ${this.post.createdAt} AND id != @postID AND status LIKE '%published%' AND status NOT LIKE '%trashed%' AND status NOT LIKE '%is-page%' AND status NOT LIKE '%hidden%' ORDER BY ${temporaryPostsOrdering} LIMIT 1 `).get({ postID: this.post.id }); } if(!postData || !postData.id) { return false; } this[postType] = this.renderer.cachedItems.posts[postData.id]; } loadRelatedPosts() { let renderRelatedPosts = RendererHelpers.getRendererOptionValue('renderRelatedPosts', this.themeConfig); let relatedPostsNumberFromConfig = RendererHelpers.getRendererOptionValue('relatedPostsNumber', this.themeConfig); if (!renderRelatedPosts || relatedPostsNumberFromConfig === 0) { this.relatedPosts = []; return; } this.post.id = parseInt(this.post.id, 10); let tags = this.post.tags ? this.post.tags.map(tag => parseInt(tag.id, 10)) : []; let relatedPostsNumber = 5; let tagsCondition = ''; let postTitleConditions = []; let conditions = []; if (relatedPostsNumberFromConfig) { relatedPostsNumber = relatedPostsNumberFromConfig; } // Get tags if(tags.length) { tagsCondition = ' pt.tag_id IN(' + tags.join(',') + ') '; conditions.push(tagsCondition); } // Get words to compare (with length bigger than 3 chars) let stringsToCompare = this.post.title.split(' '); stringsToCompare = stringsToCompare.filter(word => word.length > 3); if(stringsToCompare.length) { for (let toCompare of stringsToCompare) { postTitleConditions.push(' LOWER(p.title) LIKE LOWER(\'%' + sqlString.escape(toCompare).replace(/'/g, '').replace(/"/g, '') + '%\') ') } postTitleConditions = '(' + postTitleConditions.join('OR') + ')'; conditions.push(postTitleConditions); } if(conditions.length > 1) { conditions = conditions.join(' OR '); conditions = ' ( ' + conditions + ' ) '; } if(conditions.length) { conditions = ' AND ' + conditions; } // Retrieve post let postsData = this.db.prepare(` SELECT p.id AS id FROM posts AS p LEFT JOIN posts_tags AS pt ON p.id = pt.post_id WHERE p.id != @postID AND p.status LIKE '%published%' AND p.status NOT LIKE '%trashed%' AND p.status NOT LIKE '%is-page%' AND p.status NOT LIKE '%hidden%' ${conditions} GROUP BY p.id LIMIT @relatedPostsNumber `).all({ postID: this.post.id, relatedPostsNumber: relatedPostsNumber }); this.relatedPosts = []; if(!postsData || !postsData.length) { return false; } for(let i = 0; i < postsData.length; i++) { this.relatedPosts[i] = this.renderer.cachedItems.posts[postsData[i].id]; } } setContext() { this.loadData(); this.prepareData(); let metaRobotsValue = this.metaRobots; if(this.siteConfig.advanced.noIndexThisPage) { metaRobotsValue = 'noindex,nofollow'; } this.context = { title: this.metaTitle !== '' ? this.metaTitle : this.title, post: this.post, tags: this.allTags, metaTitleRaw: this.metaTitle, metaDescriptionRaw: this.metaDescription, metaRobotsRaw: metaRobotsValue, previousPost: this.previousPost, nextPost: this.nextPost, siteOwner: this.renderer.cachedItems.authors[1], menus: this.menus.assigned }; } getContext(postID) { this.postID = postID; this.setContext(); return this.context; } getPostFeaturedImages(postID, mainPost = false) { let postImage = false; // Retrieve post image if(mainPost === true) { postImage = { id: 0, url: this.renderer.postData.featuredImageFilename, additional_data: JSON.stringify(this.renderer.postData.featuredImageData) }; } else { postImage = this.db.prepare(` SELECT pi.id AS id, pi.url AS url, pi.additional_data AS additional_data FROM posts as p LEFT JOIN posts_images as pi ON p.featured_image_id = pi.id WHERE p.id = @postID ORDER BY pi.id DESC LIMIT 1 `).get({ postID: postID }); } if (postImage && postImage.url) { let url = ''; let alt = ''; let caption = ''; let credits = ''; let imageDimensions = false; if (postImage.additional_data) { let data = JSON.parse(postImage.additional_data); let postDirectory = postID; if(postDirectory === 0) { postDirectory = 'temp'; } let imagePath = URLHelper.createImageURL(this.inputDir, postDirectory, postImage.url); let domain = this.siteConfig.domain; url = URLHelper.createImageURL(domain, postDirectory, postImage.url); alt = data.alt; caption = data.caption; credits = data.credits; try { imageDimensions = sizeOf(imagePath); } catch(e) { console.log('post-preview.js: wrong image path - missing dimensions'); imageDimensions = false; } } else { return false; } let featuredImageSrcSet = false; let featuredImageSizes = false; if(!this.isGifOrSvg(url)) { let useWebp = false; if (this.renderer.siteConfig?.advanced?.forceWebp) { useWebp = true; } featuredImageSrcSet = ContentHelper.getFeaturedImageSrcset(url, this.themeConfig, useWebp); featuredImageSizes = ContentHelper.getFeaturedImageSizes(this.themeConfig); } else { featuredImageSrcSet = ''; featuredImageSizes = ''; } let featuredImageData = { id: postImage.id, url: url, alt: alt, caption: caption, credits: credits, height: imageDimensions.height, width: imageDimensions.width, srcset: featuredImageSrcSet, sizes: featuredImageSizes }; // Create alternative names for dimensions let dimensions = false; if ( this.themeConfig.files && this.themeConfig.files.responsiveImages ) { if ( this.themeConfig.files.responsiveImages.featuredImages && this.themeConfig.files.responsiveImages.featuredImages.dimensions ) { dimensions = this.themeConfig.files.responsiveImages.featuredImages.dimensions; } else if ( this.themeConfig.files.responsiveImages.contentImages && this.themeConfig.files.responsiveImages.contentImages.dimensions ) { dimensions = this.themeConfig.files.responsiveImages.featuredImages.dimensions; } if (dimensions) { let dimensionNames = Object.keys(dimensions); for (let dimensionName of dimensionNames) { let base = path.parse(url).base; let filename = path.parse(url).name; let extension = path.parse(url).ext; let newFilename = filename + '-' + dimensionName + extension; let capitalizedDimensionName = dimensionName.charAt(0).toUpperCase() + dimensionName.slice(1); if(!this.isGifOrSvg(url)) { featuredImageData['url' + capitalizedDimensionName] = url.replace(base, newFilename); } else { featuredImageData['url' + capitalizedDimensionName] = url; } } } } return featuredImageData; } return false; } prepareContent(originalText, postID) { let self = this; let domain = normalizePath(self.siteConfig.domain); domain = URLHelper.fixProtocols(domain); // Get media URL let postDirectory = postID; if(postDirectory === 0) { postDirectory = 'temp'; } let domainMediaPath = domain + '/media/posts/' + postDirectory + '/'; // Replace domain name constat with real URL to media directory let preparedText = originalText.split('#DOMAIN_NAME#').join(domainMediaPath); preparedText = ContentHelper.parseText(preparedText, this.editor); // Remove TOC plugin ID attributes when TOC does not exist if (preparedText.indexOf('class="post__toc') === -1) { preparedText = preparedText.replace(/\sid="mcetoc_[a-z0-9]*?"/gmi, ''); } // Reduce download="download" to download preparedText = preparedText.replace(/download="download"/gmi, 'download'); // Remove content for AMP or non-AMP depending from ampMode value preparedText = preparedText.replace(/.*<\/publii-amp>/gmi, ''); preparedText = preparedText.replace(//gmi, ''); preparedText = preparedText.replace(/<\/publii-non-amp>/gmi, ''); // Remove read more text preparedText = preparedText.replace(/\/gmi, ''); // Remove the last empty paragraph preparedText = preparedText.replace(/

 <\/p>\s?$/gmi, ''); let useWebp = false; if (this.renderer.siteConfig?.advanced?.forceWebp) { useWebp = true; } // Find all images and add srcset and sizes attributes if (this.siteConfig.responsiveImages) { preparedText = preparedText.replace(/].*?\sloading="[^>].*?>)/gmi, '].*?\sloading="[^>].*?>)/gmi, '].*?\sloading="[^>].*?>)/gmi, '].*?\sloading="[^>].*?>)/gmi, ' preparedText = preparedText.replace(/(\s*?)?]*?(class=".*?").*?>(\s*?<\/p>)?/gmi, function(matches, p1, classes) { return '

' + matches.replace('

', '').replace(//, '').replace(classes, '') + '
'; }); // Fix some specific syntax cases for double figure elements preparedText = preparedText.replace(/
[\s]*?
([\s\S]*?)<\/figure>[\s]*?<\/figure>/gmi, '
$1
'); preparedText = preparedText.replace(/
[\s]*?
([\s\S]*?)<\/figure>[\s]*?
([\s\S]*?)<\/figcaption>[\s]*?<\/figure>/gmi, '
$1
$2
'); } // Remove contenteditable attributes preparedText = preparedText.replace(/contentEditable=".*?"/gi, ''); if (this.editor === 'tinymce') { // Wrap galleries with classes into div with gallery-wrapper CSS class preparedText = preparedText.replace(/
?/gmi, function(matches, classes) { return ''; }); } // Remove paragraphs around '); // Wrap iframes into
preparedText = preparedText.replace(/(?[\s\S]*?)([\s\S]*?<\/iframe>)/gmi, function(matches) { if (matches.indexOf('data-responsive="false"') > -1) { return matches; } return '
' + matches + '
'; }); // Remove CDATA sections inside scripts added by TinyMCE preparedText = preparedText.replace(/\\/\/ \<\!\[CDATA\[/g, ''); return preparedText; } /** * Detects if image is a GIF or SVG */ isGifOrSvg(url) { if(url.slice(-4) === '.gif' || url.slice(-4) === '.svg') { return true; } return false; } } module.exports = RendererContextPostPreview; ================================================ FILE: app/back-end/modules/render-html/contexts/post.js ================================================ // Necessary packages const RendererContext = require('../renderer-context.js'); const RendererHelpers = require('./../helpers/helpers.js'); const sqlString = require('sqlstring'); const stripTags = require('striptags'); /** * Class used create context * for the single post theme views */ class RendererContextPost extends RendererContext { loadData() { // Retrieve meta data let metaDataQuery = this.db.prepare(`SELECT value FROM posts_additional_data WHERE post_id = @postID AND key = '_core'`); this.metaData = metaDataQuery.get({ postID: this.postID}); this.allTags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true); this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true); this.menus = this.renderer.commonData.menus; this.unassignedMenus = this.renderer.commonData.unassignedMenus; this.authors = this.renderer.commonData.authors; this.featuredPosts = this.renderer.commonData.featuredPosts.homepage; this.hiddenPosts = this.renderer.commonData.hiddenPosts; this.pages = this.renderer.commonData.pages; // mark tags as main tags let mainTagsIds = this.mainTags.map(tag => tag.id); this.allTags = this.allTags.map(tag => { tag.isMainTag = mainTagsIds.includes(tag.id); return tag; }); } prepareData() { this.post = this.renderer.cachedItems.posts[this.postID]; this.post.tags = this.post.tags.filter(tag => tag.additionalData.isHidden !== true); this.featuredPosts = this.featuredPosts || []; this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.hiddenPosts = this.hiddenPosts || []; this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.pages = this.pages || []; this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]); this.metaTitle = this.siteConfig.advanced.postMetaTitle; this.metaDescription = this.siteConfig.advanced.postMetaDescription; this.canonicalUrl = this.post.url; this.hasCustomCanonicalUrl = false; this.metaRobots = ''; if (this.siteConfig.advanced.postMetaDescription === '') { this.metaDescription = stripTags(this.post.excerpt).replace(/\n/gmi, ''); } if(this.metaData && this.metaData.value) { let results = JSON.parse(this.metaData.value); if (results.metaTitle) { this.metaTitle = results.metaTitle; } if (results.metaDesc) { this.metaDescription = results.metaDesc; } if (results.metaRobots) { this.metaRobots = results.metaRobots; } if (results.canonicalUrl) { this.canonicalUrl = results.canonicalUrl; this.hasCustomCanonicalUrl = true; this.metaRobots = ''; } } let siteName = this.siteConfig.name; if(this.siteConfig.displayName) { siteName = this.siteConfig.displayName; } if (this.metaTitle === '') { this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName); } if (this.metaDescription === '') { this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName); } // load related posts this.loadRelatedPosts(); // load previous and next posts (only on the visible posts) let renderPrevNextPosts = RendererHelpers.getRendererOptionValue('renderPrevNextPosts', this.themeConfig); if(!renderPrevNextPosts || this.post.status.indexOf('hidden') > -1) { this.nextPost = false; this.previousPost = false; } else { this.loadPost('next', false); this.loadPost('previous', false); } // load previous and next similar posts (only on the visible posts) let renderSimilarPosts = RendererHelpers.getRendererOptionValue('renderSimilarPosts', this.themeConfig); if(!renderSimilarPosts || this.post.status.indexOf('hidden') > -1) { this.nextSimilarPost = false; this.previousSimilarPost = false; } else { this.loadPost('next', true); this.loadPost('previous', true); } } loadPost(type, similarPost = false) { let postType = similarPost ? type + 'SimilarPost' : type + 'Post'; let operator = type === 'previous' ? '<=' : '>='; let postData = false; let temporaryPostsOrdering = this.postsOrdering; // Reverse operator when post ordering is reversed if(temporaryPostsOrdering.indexOf('ASC') > -1) { if(operator === '>=') { operator = '<='; } else { operator = '>='; } } // For the next posts we have to reverse the results if(type === 'next') { if(temporaryPostsOrdering.indexOf('ASC') > -1) { temporaryPostsOrdering = temporaryPostsOrdering.replace('ASC', 'DESC'); } else { temporaryPostsOrdering = temporaryPostsOrdering.replace('DESC', 'ASC'); } } this.post.createdAt = parseInt(this.post.createdAt, 10); this.post.id = parseInt(this.post.id, 10); let tagsCondition = ''; let sortColumn = this.siteConfig.advanced.postsListingOrderBy; if (typeof sortColumn !== 'string') { sortColumn = 'created_at'; } let sortField = sortColumn; if (sortColumn === 'modified_at') { sortField = 'modifiedAt'; } else if (sortColumn === 'created_at') { sortField = 'createdAt'; } if(similarPost) { let tags = this.post.tags ? this.post.tags.map(tag => parseInt(tag.id, 10)) : []; if(tags.length) { tagsCondition = ' AND pt.tag_id IN(' + tags.join(',') + ') '; } let sortCondition = `p.${sortColumn} ${operator} ${this.post[sortField]}`; if (sortColumn === 'title') { sortCondition = `p.${sortColumn} ${operator} "${this.post[sortField].replace(/"/gmi, '')}"`; } // Retrieve post postData = this.db.prepare(` SELECT p.id AS id FROM posts AS p LEFT JOIN posts_tags AS pt ON p.id = pt.post_id WHERE ${sortCondition} AND p.id != @postID AND p.status LIKE '%published%' AND p.status NOT LIKE '%trashed%'AND p.status NOT LIKE '%is-page%' AND p.status NOT LIKE '%hidden%' ${tagsCondition} GROUP BY p.id ORDER BY ${temporaryPostsOrdering} LIMIT 1 `).get({ postID: this.post.id }); } else { // Retrieve post let sortCondition = `${sortColumn} ${operator} ${this.post[sortField]}`; if (sortColumn === 'title') { sortCondition = `${sortColumn} ${operator} "${this.post[sortField].replace(/"/gmi, '')}"`; } try { postData = this.db.prepare(` SELECT id FROM posts WHERE ${sortCondition} AND id != @postID AND status LIKE '%published%' AND status NOT LIKE '%trashed%' AND status NOT LIKE '%is-page%' AND status NOT LIKE '%hidden%' ORDER BY ${temporaryPostsOrdering} LIMIT 1 `).get({ postID: this.post.id }); } catch (err) { console.log('ERR', err); } } if(!postData || !postData.id) { return false; } this[postType] = this.renderer.cachedItems.posts[postData.id]; } loadRelatedPosts() { let renderRelatedPosts = RendererHelpers.getRendererOptionValue('renderRelatedPosts', this.themeConfig); let relatedPostsNumberFromConfig = RendererHelpers.getRendererOptionValue('relatedPostsNumber', this.themeConfig); if (!renderRelatedPosts || relatedPostsNumberFromConfig === 0) { this.relatedPosts = []; return; } this.post.id = parseInt(this.post.id, 10); let tags = this.post.tags ? this.post.tags.map(tag => parseInt(tag.id, 10)) : []; let relatedPostsNumber = 5; let postTitleConditions = []; let conditions = []; let conditionsLowerPriority = []; if (relatedPostsNumberFromConfig) { relatedPostsNumber = relatedPostsNumberFromConfig; } // Get tags if(tags.length) { conditions.push(' pt.tag_id IN(' + tags.join(',') + ') '); conditionsLowerPriority.push(' pt.tag_id NOT IN(' + tags.join(',') + ') '); } // Get words to compare (with length bigger than 3 chars) if (['titles', 'titles-and-tags'].indexOf(this.siteConfig.advanced.relatedPostsCriteria) > -1) { let stringsToCompare = this.post.title.split(' '); stringsToCompare = stringsToCompare.filter(word => word.length > 3); if(stringsToCompare.length) { for (let toCompare of stringsToCompare) { postTitleConditions.push(' LOWER(p.title) LIKE LOWER(\'%' + sqlString.escape(toCompare).replace(/'/g, '').replace(/"/g, '') + '%\') ') } postTitleConditions = '(' + postTitleConditions.join('OR') + ')'; conditions.push(postTitleConditions); conditionsLowerPriority.push(postTitleConditions); } } if(conditions.length > 1) { conditions = conditions.join(' AND '); conditions = ' ( ' + conditions + ' ) '; } if(conditions.length) { conditions = ' AND ' + conditions; } if(conditionsLowerPriority.length > 1) { conditionsLowerPriority = conditionsLowerPriority.join(' AND '); conditionsLowerPriority = ' ( ' + conditionsLowerPriority + ' ) '; } if(conditionsLowerPriority.length) { conditionsLowerPriority = ' AND ' + conditionsLowerPriority; } // Get related posts ordering let ordering = ' subquery.id DESC '; if (this.siteConfig.advanced.relatedPostsOrder === 'id-asc') { ordering = ' subquery.id ASC '; } else if (this.siteConfig.advanced.relatedPostsOrder === 'random') { ordering = ' RANDOM() '; } // Use second query let secondQuery = ''; if (this.siteConfig.advanced.relatedPostsIncludeAllPosts) { secondQuery = ` UNION SELECT p.id AS id, 2 AS priority FROM posts AS p LEFT JOIN posts_tags AS pt ON p.id = pt.post_id WHERE p.id != @postID AND p.status LIKE '%published%' AND p.status NOT LIKE '%trashed%' AND p.status NOT LIKE '%is-page%' AND p.status NOT LIKE '%hidden%' ${conditionsLowerPriority} `; } // Retrieve post let postsData = this.db.prepare(` SELECT subquery.id AS id FROM ( SELECT p.id AS id, 1 AS priority FROM posts AS p LEFT JOIN posts_tags AS pt ON p.id = pt.post_id WHERE p.id != @postID AND p.status LIKE '%published%' AND p.status NOT LIKE '%trashed%' AND p.status NOT LIKE '%is-page%' AND p.status NOT LIKE '%hidden%' ${conditions} ${secondQuery} GROUP BY p.id ORDER BY priority ASC LIMIT @relatedPostsNumber ) AS subquery ORDER BY ${ordering} `).all({ postID: this.post.id, relatedPostsNumber: relatedPostsNumber * 2 }); this.relatedPosts = []; if (!postsData || !postsData.length) { return false; } postsData = postsData.map(postData => postData.id); postsData = [...new Set(postsData)]; if (postsData.length > relatedPostsNumber) { postsData = postsData.slice(0, relatedPostsNumber); } for(let i = 0; i < postsData.length; i++) { this.relatedPosts[i] = this.renderer.cachedItems.posts[postsData[i]]; } } setContext() { this.loadData(); this.prepareData(); let metaRobotsValue = this.metaRobots; if(this.siteConfig.advanced.noIndexThisPage) { metaRobotsValue = 'noindex,nofollow'; } let siteName = this.siteConfig.name; if(this.siteConfig.displayName) { siteName = this.siteConfig.displayName; } // Detect if the post title is empty if(this.metaTitle === '') { this.metaTitle = this.siteConfig.advanced.postMetaTitle.replace(/%posttitle/g, this.post.title) .replace(/%sitename/g, siteName) .replace(/%authorname/g, this.post.author.name); } else { this.metaTitle = this.metaTitle.replace(/%posttitle/g, this.post.title) .replace(/%sitename/g, siteName) .replace(/%authorname/g, this.post.author.name); } // If still meta title is empty - use post title if(this.metaTitle === '') { this.metaTitle = this.post.title; } this.metaDescription = this.metaDescription.replace(/%posttitle/g, this.post.title) .replace(/%sitename/g, siteName) .replace(/%authorname/g, this.post.author.name); this.context = { title: this.metaTitle, post: this.post, featuredPosts: this.featuredPosts, hiddenPosts: this.hiddenPosts, relatedPosts: this.relatedPosts, tags: this.allTags, mainTags: this.mainTags, authors: this.authors, pages: this.pages, metaTitleRaw: this.metaTitle, metaDescriptionRaw: this.metaDescription, metaRobotsRaw: metaRobotsValue, hasCustomCanonicalUrl: this.hasCustomCanonicalUrl, canonicalUrl: this.canonicalUrl, previousPost: this.previousPost, previousSimilarPost: this.previousSimilarPost, nextPost: this.nextPost, nextSimilarPost: this.nextSimilarPost, siteOwner: this.renderer.cachedItems.authors[1], menus: this.menus, unassignedMenus: this.unassignedMenus }; } getContext(postID) { this.postID = postID; this.setContext(); return this.context; } } module.exports = RendererContextPost; ================================================ FILE: app/back-end/modules/render-html/contexts/search.js ================================================ // Necessary packages const RendererContext = require('../renderer-context.js'); /** * Class used create context * for the search theme view */ class RendererContextSearch extends RendererContext { /** * Loading data used in the view */ loadData() { let siteName = this.siteConfig.name; if(this.siteConfig.displayName) { siteName = this.siteConfig.displayName; } this.metaTitle = this.siteConfig.advanced.searchMetaTitle.replace(/%sitename/g, siteName); this.metaDescription = this.siteConfig.advanced.searchMetaDescription.replace(/%sitename/g, siteName); if (this.metaTitle === '') { this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName); } if (this.metaDescription === '') { this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName); } this.tags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true); this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true); this.menus = this.renderer.commonData.menus; this.unassignedMenus = this.renderer.commonData.unassignedMenus; this.authors = this.renderer.commonData.authors; this.featuredPosts = this.renderer.commonData.featuredPosts.homepage; this.hiddenPosts = this.renderer.commonData.hiddenPosts; this.pages = this.renderer.commonData.pages; // mark tags as main tags let mainTagsIds = this.mainTags.map(tag => tag.id); this.tags = this.tags.map(tag => { tag.isMainTag = mainTagsIds.includes(tag.id); return tag; }); } /** * Preparing the loaded data */ prepareData() { this.title = this.siteConfig.name; this.featuredPosts = this.featuredPosts || []; this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.hiddenPosts = this.hiddenPosts || []; this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.pages = this.pages || []; this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]); } /** * Setting context for the view */ setContext() { this.loadData(); this.prepareData(); let metaRobotsValue = this.siteConfig.advanced.metaRobotsSearch; if (this.siteConfig.advanced.noIndexThisPage) { metaRobotsValue = 'noindex,nofollow'; } this.context = { title: this.metaTitle !== '' ? this.metaTitle : this.title, featuredPosts: this.featuredPosts, hiddenPosts: this.hiddenPosts, tags: this.tags, mainTags: this.mainTags, authors: this.authors, pages: this.pages, metaTitleRaw: this.metaTitle, metaDescriptionRaw: this.metaDescription, metaRobotsRaw: metaRobotsValue, siteOwner: this.renderer.cachedItems.authors[1], menus: this.menus, unassignedMenus: this.unassignedMenus }; } /** * Getting context for the view * * @returns {object} - context for the view */ getContext() { this.setContext(); return this.context; } } module.exports = RendererContextSearch; ================================================ FILE: app/back-end/modules/render-html/contexts/tag.js ================================================ // Necessary packages const RendererContext = require('../renderer-context.js'); const RendererHelpers = require('./../helpers/helpers.js'); /** * Class used create context * for the single tag theme views */ class RendererContextTag extends RendererContext { loadData() { // Prepare query data this.postsNumber = parseInt(this.postsNumber, 10); this.offset = parseInt(this.offset, 10); // Retrieve tag data this.tag = this.renderer.cachedItems.tags[this.tagID]; // Retrieve post let includeFeaturedPosts = ''; let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('tagsIncludeFeaturedInPosts', this.themeConfig) === false; if (shouldSkipFeaturedPosts) { includeFeaturedPosts = 'p.status NOT LIKE \'%featured%\' AND'; } if(this.postsNumber === -1) { this.postsNumber = 999; } if(this.postsNumber === 0) { this.posts = false; } else { this.posts = this.db.prepare(` SELECT id FROM posts AS p LEFT JOIN posts_tags AS pt ON p.id = pt.post_id WHERE p.status LIKE '%published%' AND p.status NOT LIKE '%hidden%' AND p.status NOT LIKE '%trashed%' AND p.status NOT LIKE '%is-page%' AND ${includeFeaturedPosts} pt.tag_id = @tagID ORDER BY ${this.postsOrdering} LIMIT @postsNumber OFFSET @offset `).all({ tagID: this.tagID, postsNumber: this.postsNumber, offset: this.offset }); } this.tags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true); this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true); this.menus = this.renderer.commonData.menus; this.unassignedMenus = this.renderer.commonData.unassignedMenus; this.authors = this.renderer.commonData.authors; this.featuredPosts = this.renderer.commonData.featuredPosts.tag; this.hiddenPosts = this.renderer.commonData.hiddenPosts; this.pages = this.renderer.commonData.pages; // mark tags as main tags let mainTagsIds = this.mainTags.map(tag => tag.id); this.tags = this.tags.map(tag => { tag.isMainTag = mainTagsIds.includes(tag.id); return tag; }); } prepareData() { let siteName = this.siteConfig.name; if(this.siteConfig.displayName) { siteName = this.siteConfig.displayName; } this.title = this.siteConfig.advanced.tagMetaTitle .replace(/%tagname/g, this.tag.name) .replace(/%sitename/g, siteName); this.posts = this.posts || []; this.posts = this.posts.map(post => this.renderer.cachedItems.posts[post.id]); this.featuredPosts = this.featuredPosts || []; this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.hiddenPosts = this.hiddenPosts || []; this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.pages = this.pages || []; this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]); let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('tagsIncludeFeaturedInPosts', this.themeConfig) === false; let featuredPostsNumber = RendererHelpers.getRendererOptionValue('tagsFeaturedPostsNumber', this.themeConfig); // Remove featured posts from posts if featured posts allowed if (shouldSkipFeaturedPosts && (featuredPostsNumber > 0 || featuredPostsNumber === -1)) { let featuredPostsIds = this.featuredPosts.map(post => post.id); this.posts = this.posts.filter(post => featuredPostsIds.indexOf(post.id) === -1); } // Prepare meta data this.metaTitle = this.siteConfig.advanced.tagMetaTitle.replace(/%tagname/g, this.tag.name) .replace(/%sitename/g, siteName); this.metaDescription = this.siteConfig.advanced.tagMetaDescription.replace(/%tagname/g, this.tag.name) .replace(/%sitename/g, siteName); this.metaRobots = false; this.hasCustomCanonicalUrl = false; this.canonicalUrl = ''; let metaData = this.tag.additionalData; if (metaData && metaData.metaTitle) { this.metaTitle = metaData.metaTitle.replace(/%tagname/g, this.tag.name) .replace(/%sitename/g, siteName); } if(metaData && metaData.metaDescription) { this.metaDescription = metaData.metaDescription.replace(/%tagname/g, this.tag.name) .replace(/%sitename/g, siteName); } if (this.metaTitle === '') { this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName); } if (this.metaDescription === '') { this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName); } if (metaData && metaData.metaRobots) { this.metaRobots = metaData.metaRobots; } if (metaData && metaData.canonicalUrl) { this.canonicalUrl = metaData.canonicalUrl; this.hasCustomCanonicalUrl = true; this.metaRobots = ''; } } setContext() { this.loadData(); this.prepareData(); let metaRobotsValue = this.siteConfig.advanced.metaRobotsTags; if (this.metaRobots !== false) { metaRobotsValue = this.metaRobots; } if (this.siteConfig.advanced.noIndexThisPage) { metaRobotsValue = 'noindex,nofollow'; } this.context = { title: this.metaTitle !== '' ? this.metaTitle : this.title, tag: this.tag, posts: this.posts, pages: this.pages, featuredPosts: this.featuredPosts, hiddenPosts: this.hiddenPosts, tags: this.tags, mainTags: this.mainTags, authors: this.authors, metaTitleRaw: this.metaTitle, metaDescriptionRaw: this.metaDescription, metaRobotsRaw: metaRobotsValue, hasCustomCanonicalUrl: this.hasCustomCanonicalUrl, canonicalUrl: this.canonicalUrl, siteOwner: this.renderer.cachedItems.authors[1], menus: this.menus, unassignedMenus: this.unassignedMenus }; } getContext(tagID, offset = 0, postsNumber = 999) { this.offset = offset; this.postsNumber = postsNumber; this.tagID = tagID; this.setContext(); return this.context; } } module.exports = RendererContextTag; ================================================ FILE: app/back-end/modules/render-html/contexts/tags.js ================================================ // Necessary packages const RendererContext = require('../renderer-context.js'); /** * Class used create context * for the tags list theme view */ class RendererContextTags extends RendererContext { loadData() { this.tags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true); this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true); this.menus = this.renderer.commonData.menus; this.unassignedMenus = this.renderer.commonData.unassignedMenus; this.authors = this.renderer.commonData.authors; this.featuredPosts = this.renderer.commonData.featuredPosts.tag; this.hiddenPosts = this.renderer.commonData.hiddenPosts; this.pages = this.renderer.commonData.pages; // mark tags as main tags let mainTagsIds = this.mainTags.map(tag => tag.id); this.tags = this.tags.map(tag => { tag.isMainTag = mainTagsIds.includes(tag.id); return tag; }); } prepareData() { let siteName = this.siteConfig.name; if (this.siteConfig.displayName) { siteName = this.siteConfig.displayName; } this.title = this.siteConfig.advanced.tagsMetaTitle.replace(/%sitename/g, siteName); this.featuredPosts = this.featuredPosts || []; this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.hiddenPosts = this.hiddenPosts || []; this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]); this.pages = this.pages || []; this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]); // Prepare meta data this.metaTitle = this.siteConfig.advanced.tagsMetaTitle.replace(/%sitename/g, siteName); this.metaDescription = this.siteConfig.advanced.tagsMetaDescription.replace(/%sitename/g, siteName); if (this.metaTitle === '') { this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName); } if (this.metaDescription === '') { this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName); } } setContext() { this.loadData(); this.prepareData(); let metaRobotsValue = this.siteConfig.advanced.metaRobotsTagsList; if (this.siteConfig.advanced.noIndexThisPage) { metaRobotsValue = 'noindex,nofollow'; } this.context = { title: this.metaTitle !== '' ? this.metaTitle : this.title, featuredPosts: this.featuredPosts, hiddenPosts: this.hiddenPosts, pages: this.pages, tags: this.tags, tagsNumber: this.tags.length, mainTags: this.mainTags, authors: this.authors, metaTitleRaw: this.metaTitle, metaDescriptionRaw: this.metaDescription, metaRobotsRaw: metaRobotsValue, siteOwner: this.renderer.cachedItems.authors[1], menus: this.menus, unassignedMenus: this.unassignedMenus }; } getContext () { this.setContext(); return this.context; } } module.exports = RendererContextTags; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/_modules.js ================================================ /* * Module which loads all Handlebar's helpers */ module.exports = { assetHelper: require('./asset.js').assetHelper, CSSHelper: require('./css.js'), fontHelper: require('./font.js').fontHelper, dateHelper: require('./date.js'), is: require('./is.js'), isNot: require('./is-not.js'), isCurrentPage: require('./is-current-page.js'), encodeUrl: require('./encode-url.js'), encodeUrlFragment: require('./encode-url-fragment.js'), JSHelper: require('./js.js'), metaDescription: require('./meta-description.js'), metaRobotsHelper: require('./meta-robots.js'), pageURLHelper: require('./page-url.js'), menuURLHelper: require('./menu-url.js'), menuItemClassesHelper: require('./menu-item-classes.js'), feedLinkHelper: require('./feed-link.js').feedLinkHelper, socialMetaTagsHelper: require('./social-meta-tags.js'), publiiHeadHelper: require('./publii-head.js'), publiiFooterHelper: require('./publii-footer.js'), gdprScriptBlockerHelper: require('./gdpr-script-blocker.js'), checkIf: require('./check-if.js'), checkIfAny: require('./check-if-any.js'), checkIfAll: require('./check-if-all.js'), checkIfNone: require('./check-if-none.js'), isEmpty: require('./is-empty.js'), isNotEmpty: require('./is-not-empty.js'), jsonLDHelper: require('./json-ld.js'), canonicalLinkHelper: require('./canonical-link.js'), imageDimensionsHelper: require('./image-dimensions.js'), responsiveSrcSetHelper: require('./responsive-srcset.js').responsiveSrcSetHelper, responsiveSizesHelper: require('./responsive-sizes.js').responsiveSizesHelper, responsiveImageAttributesHelper: require('./responsive-image-attributes.js'), translateHelper: require('./translate.js').translateHelper, math: require('./math.js'), jsonify: require('./jsonify.js'), reverse: require('./reverse.js'), orderby: require('./orderby.js'), getPageHelper: require('./get-page.js'), getPagesHelper: require('./get-pages.js'), getPagesByCustomFieldHelper: require('./get-pages-by-custom-field.js'), getPostHelper: require('./get-post.js'), getPostsHelper: require('./get-posts.js'), getPostByTagsHelper: require('./get-post-by-tags.js'), getPostsByTagsHelper: require('./get-posts-by-tags.js'), getPostsByCustomFieldsHelper: require('./get-posts-by-custom-field.js'), getTagHelper: require('./get-tag.js'), getTagsHelper: require('./get-tags.js'), concatenate: require('./concatenate.js'), contains: require('./contains.js'), join: require('./join.js'), lazyloadHelper: require('./lazyload.js'), getAuthorHelper: require('./get-author.js'), getAuthorsHelper: require('./get-authors.js') }; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/asset.js ================================================ const Handlebars = require('handlebars'); /** * Helper function for generating asset URL based on a given path * * @param {string} path * * @returns {string} URL to the assets directory */ function asset(filePath) { let url = [ this.siteConfig.domain, this.themeConfig.files.assetsPath, filePath ].join('/'); url = Handlebars.Utils.escapeExpression(url); return new Handlebars.SafeString(url); } /** * Helper for loading asset files * * @returns {string} URL to the asset */ function assetHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('asset', asset.bind(rendererInstance)); } module.exports = { assetHelper: assetHelper, __asset: asset }; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/canonical-link.js ================================================ const Handlebars = require('handlebars'); /** * Helper for creating canonical link * * {{canonicalLink}} * * @returns {string} - element with canonical URL */ function canonicalLinkHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('canonicalLink', function (context) { // If current page is not indexed - skip canonical link if ((context.data.root.metaRobotsRaw.indexOf('noindex') > -1 || context.data.root.metaRobotsRaw.indexOf('nofollow') > -1) && !context.data.root.hasCustomCanonicalUrl) { return ''; } if ( Array.isArray(context.data.context) && context.data.context[0] && ( ( rendererInstance.siteConfig.advanced.homepageNoIndexPagination && context.data.context.indexOf('index-pagination') !== -1 ) || ( rendererInstance.siteConfig.advanced.tagNoIndexPagination && context.data.context.indexOf('tag-pagination') !== -1 ) || ( rendererInstance.siteConfig.advanced.authorNoIndexPagination && context.data.context.indexOf('author-pagination') !== -1 ) ) ) { return ''; } let pageUrl = context.data.website.pageUrl; // If current page is a post - check for canonical URL if (context.data.root.canonicalUrl) { pageUrl = context.data.root.canonicalUrl; } // Remove index.html from the end of URL pageUrl = pageUrl.replace(/index\.html$/, '', pageUrl); // Add trailing slash if not exists if(pageUrl[pageUrl.length - 1] !== '/' && pageUrl.substr(-5) !== '.html') { pageUrl = pageUrl + '/'; } let output = ''; return new Handlebars.SafeString(output); }); } module.exports = canonicalLinkHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/check-if-all.js ================================================ /** * Helper for creating conditions where all * elements are connected using AND operator * * {{#checkIfAll value1 value2 ... valueN}} * ... * {{/checkIfAll}} * * @returns {callback} - true if all provided values are true otherwhise false */ function checkIfAll() { let args = Array.from(arguments); let options = args.pop(); let allLength = args.length; let result = args.filter(argument => !!argument === true); if(result.length > 0 && result.length === allLength) { return options.fn(this); } return options.inverse(this); } module.exports = checkIfAll; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/check-if-any.js ================================================ /** * Helper for creating conditions where all * elements are connected using OR operator * * {{#checkIfAny value1 value2 ... valueN}} * ... * {{/checkIfAny}} * * @returns {callback} - true if at least one of the provided values is true otherwhise false */ function checkIfAny() { let args = Array.from(arguments); let options = args.pop(); let result = args.filter(argument => !!argument == true); if(result.length > 0) { return options.fn(this); } return options.inverse(this); } module.exports = checkIfAny; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/check-if-none.js ================================================ /** * Helper for creating conditions where all * elements are connected using AND operator * and are all false * * {{#checkIfNone value1 value2 ... valueN}} * ... * {{/checkIfNone}} * * @returns {callback|boolean} - true if any of the provided values is true otherwhise false */ function checkIfNone() { let args = Array.from(arguments); let options = args.pop(); let result = args.filter(argument => !!argument === true); if(result.length === 0) { return options.fn(this); } return options.inverse(this); } module.exports = checkIfNone; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/check-if.js ================================================ /** * Helper for creating conditions * * {{#checkIf author '&&' avatar}} * ... * {{/checkIf}} * * Available operators: * '==', '!=', '===', '!==', '&&', '||', '<', '<=', '>', '>=', * 'and', 'or', 'equal', 'strictEqual', 'different', 'strictDifferent', 'lesser', 'lesserEqual', 'greater', 'greaterEqual', 'contains', 'notContains' * * @returns {callback} */ function checkIf(v1, operator, v2, options) { if(v1 === undefined || v2 === undefined) { return; } switch (operator) { case "&&": case "and": return (v1 && v2) ? options.fn(this) : options.inverse(this); case "||": case "or": return (v1 || v2) ? options.fn(this) : options.inverse(this); case "==": case "equal": return (v1 == v2) ? options.fn(this) : options.inverse(this); case "===": case "strictEqual": return (v1 === v2) ? options.fn(this) : options.inverse(this); case "!=": case "different": return (v1 != v2) ? options.fn(this) : options.inverse(this); case "!==": case "strictDifferent": return (v1 !== v2) ? options.fn(this) : options.inverse(this); case "<": case "lesser": return (v1 < v2) ? options.fn(this) : options.inverse(this); case ">": case "greater": return (v1 > v2) ? options.fn(this) : options.inverse(this); case "<=": case "lesserEqual": return (v1 <= v2) ? options.fn(this) : options.inverse(this); case ">=": case "greaterEqual": return (v1 >= v2) ? options.fn(this) : options.inverse(this); case "contains": if (typeof v1 !== 'string') { return; } if (typeof v2 === 'number') { return (v1.split(',').map(v1item => +v1item).indexOf(v2) > -1) ? options.fn(this) : options.inverse(this); } return (v1.split(',').indexOf(v2) > -1) ? options.fn(this) : options.inverse(this); case "notContains": if (typeof v1 !== 'string') { return; } if (typeof v2 === 'number') { return (v1.split(',').map(v1item => +v1item).indexOf(v2) === -1) ? options.fn(this) : options.inverse(this); } return (v1.split(',').indexOf(v2) === -1) ? options.fn(this) : options.inverse(this); } return; } module.exports = checkIf; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/concatenate.js ================================================ const Handlebars = require('handlebars'); /** * Helper for concatenating values * * {{concatenate 'abc' 3 'def'}} * * @returns {callback} */ function concatenate () { let inputs = Array.from(arguments); inputs.pop(); return inputs.join(''); } module.exports = concatenate; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/contains.js ================================================ /** * Helper for checking if a specifc value is inside comma-separated string * * {{#contains 'abc' 'abc,def'}} * * @returns {callback} */ function contains (needle, haystack, options) { if (needle === undefined || haystack === undefined) { return; } if (typeof haystack === 'object' && haystack.string) { haystack = haystack.string; } haystack = haystack.split(','); if (typeof needle === 'number') { haystack = haystack.map(n => parseInt(n, 10)); } if (haystack.indexOf(needle) > -1) { return options.fn(this); } return options.inverse(this) } module.exports = contains; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/css.js ================================================ const fs = require('fs'); const FileHelper = require('./../../../../helpers/file.js'); const crypto = require('crypto'); const path = require('path'); const memoize = require('fast-memoize'); /** * * Helper function used to calculate MD5 sum of the given file contents * * @param {string} localPath - path to the file * @param {string} overridedLocalPath - path to the overrided version of the file * * @returns {string} - MD5 sum based on the given file contents */ function getMD5(localPath, overridedLocalPath) { let fileContent = ''; if (fs.existsSync(overridedLocalPath)) { fileContent = FileHelper.readFileSync(overridedLocalPath); } else { fileContent = FileHelper.readFileSync(localPath); } return crypto.createHash('md5').update(fileContent).digest('hex'); } const memoizedMD5 = memoize(getMD5); /** * Helper for loading CSS files from the assets directory * * It also adds MD5 sum hash as a v= param for preventing browser cache * * {{css "filepath.css"}} * * @returns {string} path to the CSS file with assigned v= param based on the file MD5 sum */ function CSSHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('css', function (filename) { let md5Sum = ''; let localPath = path.join(rendererInstance.inputDir, 'themes', rendererInstance.themeConfig.name.toLowerCase(), rendererInstance.themeConfig.files.assetsPath, 'css', filename); let overridedLocalPath = path.join(rendererInstance.inputDir, 'themes', rendererInstance.themeConfig.name.toLowerCase() + '-override', rendererInstance.themeConfig.files.assetsPath, 'css', filename); let versionSuffix = ''; if (rendererInstance.siteConfig.advanced.versionSuffix) { md5Sum = memoizedMD5(localPath, overridedLocalPath); versionSuffix = '?v=' + md5Sum; } let url = [ rendererInstance.siteConfig.domain, rendererInstance.themeConfig.files.assetsPath, 'css', filename + versionSuffix ].join('/'); return new Handlebars.SafeString(url); }); } module.exports = CSSHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/date.js ================================================ const Handlebars = require('handlebars'); const moment = require('moment'); /** * Helper for creating customized date output * * {{date timestamp "format"}} * * {{date timestamp "format" true}} * * {{date timestamp "format" false "en"}} * * Format compatible with moment().js: http://momentjs.com/ * * The last param allows us to not return safe string which can be helpful for time comparision * * @returns {string} - date formatted using specified format * */ function dateHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('date', function (timestamp, dateFormat, returnRawText = false, overrideDateLanguage = false) { // If date format is not a string - then it is empty or is an object with // options for the helper if(typeof dateFormat !== 'string') { dateFormat = 'MMM Do YYYY'; } let originalMomentLanguage = moment.locale(); if (overrideDateLanguage) { moment.locale(overrideDateLanguage); } if(!rendererInstance.siteConfig.language || (rendererInstance.siteConfig.domain !== '' && rendererInstance.siteConfig.domain !== 'en-us')) { moment.locale(rendererInstance.siteConfig.language); } let output = moment(timestamp).format(dateFormat); if (overrideDateLanguage) { moment.locale(originalMomentLanguage); } if (returnRawText) { return output; } return new Handlebars.SafeString(output); }); } module.exports = dateHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/encode-url-fragment.js ================================================ const Handlebars = require('handlebars'); /** * Helper for creating encoded output inside URLs * * {{encodeUrlFragment "text"}} * * @returns {string} - text prepared to be displayed as a part of URLs */ function encodeUrlFragment(text) { let output = encodeURIComponent(text); return new Handlebars.SafeString(output); } module.exports = encodeUrlFragment; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/encode-url.js ================================================ const Handlebars = require('handlebars'); /** * Helper for creating encoded output of URLs * * Useful for the social media scripts where you have to put * full URL to your website as an URL param * * {{encodeUrl "text"}} * * @returns {string} - text prepared to be displayed as an URL inside other URLs */ function encodeUrl(text) { let output = encodeURI(text); return new Handlebars.SafeString(output); } module.exports = encodeUrl; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/feed-link.js ================================================ const Handlebars = require('handlebars'); /** * Helper for creating link element with feed URL (for RSS and JSON) * * {{feedLink}} * * @returns {string} elements with URL to the RSS/JSON feeds */ function feedLink() { let output = ''; if (!this.siteConfig.deployment || !this.siteConfig.deployment.relativeUrls) { let feedTitle = this.siteConfig.displayName; if ( this.siteConfig.advanced && this.siteConfig.advanced.feed && this.siteConfig.advanced.feed.title === 'customTitle' ) { feedTitle = this.siteConfig.advanced.feed.titleValue; } let rssFeedTitle = ''; let jsonFeedTitle = ''; if (feedTitle) { rssFeedTitle = 'title="' + Handlebars.Utils.escapeExpression(feedTitle) + ' - RSS"'; jsonFeedTitle = 'title="' + Handlebars.Utils.escapeExpression(feedTitle) + ' - JSON"'; } if (this.siteConfig.advanced.feed.enableRss) { let rssUrl = Handlebars.Utils.escapeExpression(this.siteConfig.domain + '/feed.xml'); output += '' + "\n"; } if (this.siteConfig.advanced.feed.enableJson) { let jsonUrl = Handlebars.Utils.escapeExpression(this.siteConfig.domain + '/feed.json'); output += '' + "\n"; } } return new Handlebars.SafeString(output); } function feedLinkHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('feedLink', feedLink.bind(rendererInstance)); } module.exports = { __feedLink: feedLink, feedLinkHelper: feedLinkHelper }; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/font.js ================================================ const Handlebars = require('handlebars'); /** * Helper function for generating Google Fonts API URL based on a given font * * @param {string} path to the Google Font font * * @returns {string} URL to the Google Fonts API for a given font */ function font(path) { let url = 'https://fonts.googleapis.com/css?family=' + path; url = Handlebars.Utils.escapeExpression(url); return new Handlebars.SafeString(url); } /** * Helper for loading font files from the Google Fonts directory * * {{font @config.visual.font}} * * @returns {string} URL to the Google Fonts API for a given font */ function fontHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('font', font.bind(rendererInstance)); } module.exports = { fontHelper: fontHelper, __font: font }; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/gdpr-script-blocker.js ================================================ const Handlebars = require('handlebars'); /** * Helper for creating proper script type value according to the * * {{gdprScriptBlocker "group-name"}} * * @returns {string} type attribute value with a proper value if GDRP is enabled or not */ function gdprScriptBlockerHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('gdprScriptBlocker', function (groupName) { let output = 'text/javascript'; if (rendererInstance.siteConfig.advanced.gdpr.enabled) { output = 'gdpr-blocker/' + groupName; } return new Handlebars.SafeString(output); }); } module.exports = gdprScriptBlockerHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/get-author.js ================================================ /** * Helper for loading author data * * {{#getAuthor AUTHOR_ID}} *

{{ name }}

* {{/getAuthor}} * * {{#getAuthor "AUTHOR_USERNAME"}} *

{{ name }}

* {{/getAuthor}} * * IMPORTANT: It requires availability of the @website.contentStructure global variable */ function getAuthorHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('getAuthor', function (selectedAuthor, options) { if (!rendererInstance.contentStructure.authors) { return 'Error: @website.contentStructure global variable is not available.'; } let authorData; if (typeof selectedAuthor === 'number') { authorData = rendererInstance.contentStructure.authors.filter(author => author.id === selectedAuthor); } else { authorData = rendererInstance.contentStructure.authors.filter(author => author.username === selectedAuthor); } if(!authorData.length) { return ''; } return options.fn(authorData[0]); }); } module.exports = getAuthorHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/get-authors.js ================================================ /** * Helper for loading authors data * * {{#getAuthors "AUTHOR_ID_1,AUTHOR_ID_2,AUTHOR_ID_N" "prefix" "suffix"}} *
  • {{ name }}
  • * {{/getAuthors}} * * Authors are ordered by the ID order in the string. * * The second parameter creates HTML prefix, the third parameter creates HTML suffix for the generated output. * * IMPORTANT: It requires availability of the @website.contentStructure global variable */ function getAuthorsHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('getAuthors', function (authorsIDs, prefix, suffix, options) { if (!rendererInstance.contentStructure.authors) { return 'Error: @website.contentStructure global variable is not available.'; } let content = ''; authorsIDs = authorsIDs.split(',').map(n => parseInt(n, 10)); for (let i = 0; i < authorsIDs.length; i++) { let authorData = rendererInstance.contentStructure.authors.filter(author => author.id === authorsIDs[i]); if (authorData.length) { options.data.index = i; content += options.fn(authorData[0]); } } if(content === '') { return ''; } content = [prefix, content, suffix].join(''); return content; }); } module.exports = getAuthorsHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/get-page.js ================================================ /** * Helper for loading page data * * {{#getPage PAGE_ID}} *

    {{ title }}

    *
    {{{ excerpt }}}
    * {{/getPage}} * * IMPORTANT: It requires availability of the @website.contentStructure global variable */ function getPageHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('getPage', function (pageID, options) { if (!rendererInstance.contentStructure.pages) { return 'Error: @website.contentStructure global variable is not available.'; } let pageData = rendererInstance.contentStructure.pages.filter(page => page.id === pageID); if(!pageData.length) { return ''; } return options.fn(pageData[0]); }); } module.exports = getPageHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/get-pages-by-custom-field.js ================================================ /** * Helper for loading pages data which contains a specific custom fields * * QueryString options: * * count - how many pages should be included in the result * * customField - which custom field should be used * * customFieldValue - which value of custom field is expected * * customFieldCompare - default: 'equals', other available values: 'not-equals', 'greater', 'greater-equals', 'lesser', 'lesser-equals' (for numeric values), 'starts-with', 'ends-with', 'contains', 'not-contains' (for string values) * * excluded - which pages should be excluded * * offset - how many pages to skip * * orderby - order field or customField * * ordering - order direction - asc, desc, random * * orderbyCompareLanguage - if orderby=customField, you can specify in which language ordering will be done. * * {{#getPagesByCustomFields "count=5&customField=test&customFieldValue=10&customFieldCompare=not-equals&excluded=1,2&offset=10&orderby=modified_at&ordering=desc"}} *

    {{ title }}

    *
    {{{ excerpt }}}
    * {{/getPagesByCustomFields}} * * IMPORTANT: It requires availability of the @website.contentStructure global variable */ function getPagesByCustomFieldsHelper (rendererInstance, Handlebars) { Handlebars.registerHelper('getPagesByCustomFields', function (queryString, options) { if (!rendererInstance.contentStructure.pages) { return 'Error: @website.contentStructure global variable is not available.'; } let count; let offset = 0; let orderby = false; let ordering = 'desc'; let customField = false; let customFieldValue = false; let customFieldCompare = 'equals'; let compareLanguage = false; queryString = queryString.split('&').map(pair => pair.split('=')); let queryStringData = {}; for (let i = 0; i < queryString.length; i++) { let key = queryString[i][0]; let value = queryString[i][1]; queryStringData[key] = value; } if (queryStringData['count']) { count = parseInt(queryStringData['count'], 10); if (count === -1) { count = 999; } } if (queryStringData['excluded']) { excludedPages = queryStringData['excluded']; } if (queryStringData['customField']) { customField = queryStringData['customField']; } if (queryStringData['customFieldValue']) { customFieldValue = queryStringData['customFieldValue']; } if (queryStringData['customFieldCompare']) { customFieldCompare = queryStringData['customFieldCompare']; } if (queryStringData['offset']) { offset = parseInt(queryStringData['offset']); } if (queryStringData['orderby']) { orderby = queryStringData['orderby']; } if (queryStringData['ordering']) { ordering = queryStringData['ordering']; } if (queryStringData['orderbyCompareLanguage']) { compareLanguage = queryStringData['orderbyCompareLanguage']; } let pagesData; let content = ''; let filteredPages = JSON.parse(JSON.stringify(rendererInstance.contentStructure.pages)); if (typeof excludedPages === 'number' || (typeof excludedPages === 'string' && excludedPages !== '')) { if (typeof excludedPages === 'number') { let excludedPage = excludedPages; filteredPages = filteredPages.filter(page => page.id !== excludedPage) } else { excludedPages = excludedPages.split(',').map(n => parseInt(n, 10)); filteredPages = filteredPages.filter(page => excludedPages.indexOf(page.id) === -1); } } filteredPages = filteredPages.filter(page => { if (!page.pageViewConfig[customField]) { return false; } switch (customFieldCompare) { case 'equals': return page.pageViewConfig[customField] == customFieldValue; case 'not-equals': return page.pageViewConfig[customField] != customFieldValue; case 'greater': return parseInt(page.pageViewConfig[customField], 10) > parseInt(customFieldValue, 10); case 'greater-equals': return parseInt(page.pageViewConfig[customField], 10) >= parseInt(customFieldValue, 10); case 'lesser': return parseInt(page.pageViewConfig[customField], 10) < parseInt(customFieldValue, 10); case 'lesser-equals': return parseInt(page.pageViewConfig[customField], 10) <= parseInt(customFieldValue, 10); case 'starts-with': return page.pageViewConfig[customField].indexOf(customFieldValue) === 0; case 'ends-with': return page.pageViewConfig[customField].lastIndexOf(customFieldValue) === page.pageViewConfig[customField].length - customFieldValue.length; case 'contains': return page.pageViewConfig[customField].indexOf(customFieldValue) !== -1; case 'not-contains': return page.pageViewConfig[customField].indexOf(customFieldValue) === -1; } }); pagesData = filteredPages; if (orderby && ordering && ordering !== 'random') { pagesData.sort((itemA, itemB) => { if (orderby === 'customField') { if (isNaN(itemA.pageViewConfig[customField]) && isNaN(itemB.pageViewConfig[customField])) { if (ordering === 'asc') { if (compareLanguage) { return itemA.pageViewConfig[customField].localeCompare(itemB.pageViewConfig[customField], compareLanguage); } else { return itemA.pageViewConfig[customField].localeCompare(itemB.pageViewConfig[customField]); } } else { if (compareLanguage) { return -(itemA.pageViewConfig[customField].localeCompare(itemB.pageViewConfig[customField], compareLanguage)); } else { return -(itemA.pageViewConfig[customField].localeCompare(itemB.pageViewConfig[customField])); } } } else { if (ordering === 'asc') { return parseInt(itemA.pageViewConfig[customField], 10) - parseInt(itemB.pageViewConfig[customField], 10); } else { return parseInt(itemB.pageViewConfig[customField], 10) - parseInt(itemA.pageViewConfig[customField], 10); } } } if (orderby !== 'customField') { if(typeof itemA[orderby] === 'string') { if (ordering === 'asc') { return itemA[orderby].localeCompare(itemB[orderby]); } else { return -(itemA[orderby].localeCompare(itemB[orderby])); } } else { if (ordering === 'asc') { return itemA[orderby] - itemB[orderby]; } else { return itemB[orderby] - itemA[orderby]; } } } }); } else if (ordering === 'random') { pagesData.sort(() => 0.5 - Math.random()); } for (let i = offset; i < count + offset; i++) { if (pagesData.length >= i + 1) { options.data.index = i; content += options.fn(pagesData[i]); } else { break; } } if (content === '') { return ''; } return content; }); } module.exports = getPagesByCustomFieldsHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/get-pages.js ================================================ /** * Helper for loading posts data * * {{#getPages "PAGE_ID_1,PAGE_ID_2,PAGE_ID_N" "prefix" "suffix"}} *
    *

    {{ title }}

    *
    {{{ excerpt }}}
    *
    * {{/getPages}} * * Pages are ordered by the ID order in the string. * * The second parameter creates HTML prefix, the third parameter creates HTML suffix for the generated output. * * IMPORTANT: It requires availability of the @website.contentStructure global variable */ function getPagesHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('getPages', function (pageIDs, prefix, suffix, options) { if (!rendererInstance.contentStructure.pages) { return 'Error: @website.contentStructure global variable is not available.'; } let content = ''; pageIDs = pageIDs.split(',').map(n => parseInt(n, 10)); for (let i = 0; i < pageIDs.length; i++) { let pageData = rendererInstance.contentStructure.pages.filter(page => page.id === pageIDs[i]); if (pageData.length) { options.data.index = i; content += options.fn(pageData[0]); } } if (content === '') { return ''; } content = [prefix, content, suffix].join(''); return content; }); } module.exports = getPagesHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/get-post-by-tags.js ================================================ /** * Helper for loading single post data which contains a specific tag(s) - specified by tag ID or tag slugs separated by comma * * Get post which contains a tag with given ID * * {{#getPostByTags TAG_ID1 ""}} *

    {{ title }}

    *
    {{{ excerpt }}}
    * {{/getPostByTags}} * * Get post which contains one of the given tag slugs * * {{#getPostByTags "TAG_SLUG1,TAG_SLUG2" ""}} *

    {{ title }}

    *
    {{{ excerpt }}}
    * {{/getPostByTags}} * * Get post which contains one of the given tag slugs excluding posts with ID equal to 1 or 2 * * {{#getPostByTags "TAG_SLUG1,TAG_SLUG2" "1,2"}} *

    {{ title }}

    *
    {{{ excerpt }}}
    * {{/getPostByTags}} * * IMPORTANT: It requires availability of the @website.contentStructure global variable */ function getPostByTagsHelper (rendererInstance, Handlebars) { Handlebars.registerHelper('getPostByTags', function (selectedTags, excludedPosts, options) { if (!rendererInstance.contentStructure.posts) { return 'Error: @website.contentStructure global variable is not available.'; } let postData; let filteredPosts = JSON.parse(JSON.stringify(rendererInstance.contentStructure.posts)); if (typeof excludedPosts === 'number' || typeof excludedPosts === 'string' && excludedPosts !== '') { if (typeof excludedPosts === 'number') { let excludedPost = excludedPosts; filteredPosts = filteredPosts.filter(post => post.id !== excludedPost) } else { excludedPosts = excludedPosts.split(',').map(n => parseInt(n, 10)); filteredPosts = filteredPosts.filter(post => excludedPosts.indexOf(post.id) === -1); } } if (typeof selectedTags === 'number') { let tagID = selectedTags; postData = filteredPosts.filter(post => post.tags.filter(tag => tag.id === tagID).length || post.hiddenTags.filter(tag => tag.id === tagID).length); } else { let tagsSlugs = selectedTags.split(','); postData = filteredPosts.filter(post => post.tags.filter(tag => tagsSlugs.indexOf(tag.slug) > -1).length || post.hiddenTags.filter(tag => tagsSlugs.indexOf(tag.slug) > -1).length); } if(!postData.length) { return ''; } return options.fn(postData[0]); }); } module.exports = getPostByTagsHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/get-post.js ================================================ /** * Helper for loading post data * * {{#getPost POST_ID}} *

    {{ title }}

    *
    {{{ excerpt }}}
    * {{/getPost}} * * IMPORTANT: It requires availability of the @website.contentStructure global variable */ function getPostHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('getPost', function (postID, options) { if (!rendererInstance.contentStructure.posts) { return 'Error: @website.contentStructure global variable is not available.'; } let postData = rendererInstance.contentStructure.posts.filter(post => post.id === postID); if(!postData.length) { return ''; } return options.fn(postData[0]); }); } module.exports = getPostHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/get-posts-by-custom-field.js ================================================ /** * Helper for loading posts data which contains a specific custom fields * * QueryString options: * * count - how many posts should be included in the result * * allowed - which post statuses should be included * * customField - which custom field should be used * * customFieldValue - which value of custom field is expected * * customFieldCompare - default: 'equals', other available values: 'not-equals', 'greater', 'greater-equals', 'lesser', 'lesser-equals' (for numeric values), 'starts-with', 'ends-with', 'contains', 'not-contains' (for string values) * * excluded - which posts should be excluded * * excluded_status - which posts statuses should be excluded * * offset - how many posts to skip * * orderby - order field or customField * * ordering - order direction - asc, desc, random * * orderbyCompareLanguage - if orderby=customField, you can specify in which language ordering will be done. * * {{#getPostsByCustomFields "count=5&allowed=hidden,featured&customField=test&customFieldValue=10&customFieldCompare=not-equals&excluded=1,2&offset=10&orderby=modified_at&ordering=desc"}} *

    {{ title }}

    *
    {{{ excerpt }}}
    * {{/getPostsByCustomFields}} * * IMPORTANT: It requires availability of the @website.contentStructure global variable */ function getPostsByCustomFieldsHelper (rendererInstance, Handlebars) { Handlebars.registerHelper('getPostsByCustomFields', function (queryString, options) { if (!rendererInstance.contentStructure.posts) { return 'Error: @website.contentStructure global variable is not available.'; } let count; let offset = 0; let allowedStatus = 'any'; let excludedStatus = ''; let orderby = false; let ordering = 'desc'; let customField = false; let customFieldValue = false; let customFieldCompare = 'equals'; let compareLanguage = false; queryString = queryString.split('&').map(pair => pair.split('=')); let queryStringData = {}; for (let i = 0; i < queryString.length; i++) { let key = queryString[i][0]; let value = queryString[i][1]; queryStringData[key] = value; } if (queryStringData['count']) { count = parseInt(queryStringData['count'], 10); if (count === -1) { count = 999; } } if (queryStringData['excluded']) { excludedPosts = queryStringData['excluded']; } if (queryStringData['excluded_status']) { excludedStatus = queryStringData['excluded_status']; } if (queryStringData['customField']) { customField = queryStringData['customField']; } if (queryStringData['customFieldValue']) { customFieldValue = queryStringData['customFieldValue']; } if (queryStringData['customFieldCompare']) { customFieldCompare = queryStringData['customFieldCompare']; } if (queryStringData['allowed']) { allowedStatus = queryStringData['allowed']; } if (queryStringData['offset']) { offset = parseInt(queryStringData['offset']); } if (queryStringData['orderby']) { orderby = queryStringData['orderby']; } if (queryStringData['ordering']) { ordering = queryStringData['ordering']; } if (queryStringData['orderbyCompareLanguage']) { compareLanguage = queryStringData['orderbyCompareLanguage']; } let postsData; let content = ''; let filteredPosts = JSON.parse(JSON.stringify(rendererInstance.contentStructure.posts)); if (typeof excludedPosts === 'number' || (typeof excludedPosts === 'string' && excludedPosts !== '')) { if (typeof excludedPosts === 'number') { let excludedPost = excludedPosts; filteredPosts = filteredPosts.filter(post => post.id !== excludedPost) } else { excludedPosts = excludedPosts.split(',').map(n => parseInt(n, 10)); filteredPosts = filteredPosts.filter(post => excludedPosts.indexOf(post.id) === -1); } } if (allowedStatus !== 'any') { allowedStatus = allowedStatus.split(','); if (excludedStatus !== '') { excludedStatus = excludedStatus.split(','); } filteredPosts = filteredPosts.filter(post => { if (excludedStatus) { for (let i = 0; i < excludedStatus.length; i++) { if (post.status.indexOf(excludedStatus[i]) > -1) { return false; } } } for (let i = 0; i < allowedStatus.length; i++) { if (post.status.indexOf(allowedStatus[i]) > -1) { return true; } } return false; }); } else if (allowedStatus === 'any' && excludedStatus !== '') { excludedStatus = excludedStatus.split(','); filteredPosts = filteredPosts.filter(post => { for (let i = 0; i < excludedStatus.length; i++) { if (post.status.indexOf(excludedStatus[i]) > -1) { return false; } } return true; }); } filteredPosts = filteredPosts.filter(post => { if (!post.postViewConfig[customField]) { return false; } switch (customFieldCompare) { case 'equals': return post.postViewConfig[customField] == customFieldValue; case 'not-equals': return post.postViewConfig[customField] != customFieldValue; case 'greater': return parseInt(post.postViewConfig[customField], 10) > parseInt(customFieldValue, 10); case 'greater-equals': return parseInt(post.postViewConfig[customField], 10) >= parseInt(customFieldValue, 10); case 'lesser': return parseInt(post.postViewConfig[customField], 10) < parseInt(customFieldValue, 10); case 'lesser-equals': return parseInt(post.postViewConfig[customField], 10) <= parseInt(customFieldValue, 10); case 'starts-with': return post.postViewConfig[customField].indexOf(customFieldValue) === 0; case 'ends-with': return post.postViewConfig[customField].lastIndexOf(customFieldValue) === post.postViewConfig[customField].length - customFieldValue.length; case 'contains': return post.postViewConfig[customField].indexOf(customFieldValue) !== -1; case 'not-contains': return post.postViewConfig[customField].indexOf(customFieldValue) === -1; } }); postsData = filteredPosts; if (orderby && ordering && ordering !== 'random') { postsData.sort((itemA, itemB) => { if (orderby === 'customField') { if (isNaN(itemA.postViewConfig[customField]) && isNaN(itemB.postViewConfig[customField])) { if (ordering === 'asc') { if (compareLanguage) { return itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField], compareLanguage); } else { return itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField]); } } else { if (compareLanguage) { return -(itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField], compareLanguage)); } else { return -(itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField])); } } } else { if (ordering === 'asc') { return parseInt(itemA.postViewConfig[customField], 10) - parseInt(itemB.postViewConfig[customField], 10); } else { return parseInt(itemB.postViewConfig[customField], 10) - parseInt(itemA.postViewConfig[customField], 10); } } } if (orderby !== 'customField') { if(typeof itemA[orderby] === 'string') { if (ordering === 'asc') { return itemA[orderby].localeCompare(itemB[orderby]); } else { return -(itemA[orderby].localeCompare(itemB[orderby])); } } else { if (ordering === 'asc') { return itemA[orderby] - itemB[orderby]; } else { return itemB[orderby] - itemA[orderby]; } } } }); } else if (ordering === 'random') { postsData.sort(() => 0.5 - Math.random()); } for (let i = offset; i < count + offset; i++) { if (postsData.length >= i + 1) { options.data.index = i; content += options.fn(postsData[i]); } else { break; } } if (content === '') { return ''; } return content; }); } module.exports = getPostsByCustomFieldsHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/get-posts-by-tags.js ================================================ /** * Helper for loading posts data which contains a specific tag(s) - specified by tag ID or tag slugs separated by comma * * Get up to five posts which contains a tag with given ID * * {{#getPostsByTags 5 TAG_ID1 ""}} *

    {{ title }}

    *
    {{{ excerpt }}}
    * {{/getPostByTags}} * * Get up to five posts which contains one of the given tag slugs * * {{#getPostsByTags 5 "TAG_SLUG1,TAG_SLUG2" ""}} *

    {{ title }}

    *
    {{{ excerpt }}}
    * {{/getPostsByTags}} * * Get up to five posts which contains one of the given tag slugs excluding posts with ID equal to 1 or 2 * * {{#getPostsByTags 5 "TAG_SLUG1,TAG_SLUG2" "1,2"}} *

    {{ title }}

    *
    {{{ excerpt }}}
    * {{/getPostsByTags}} * * QueryString options: * * count - how many posts should be included in the result * * allowed - which post statuses should be included * * tags - which tags should be used * * excluded - which posts should be excluded * * excluded_status - which posts statuses should be excluded * * offset - how many posts to skip * * orderby - order field or customField * * ordering - order direction - asc, desc, random * * customField - use when orderby=customField - name of the field to be used for ordering * * orderbyCompareLanguage - if orderby=customField, you can specify in which language ordering will be done. * * tag_as - specify if we select by tag id or slug * * operator - (OR or AND as value) - defines how the tags should be selected (post must have all tags at once time - AND, or one of them - OR) * * {{#getPostsByTags "count=5&allowed=hidden,featured&tag_as=id&tags=1,2,3&excluded=1,2&offset=10&orderby=modified_at&ordering=desc&operator=AND"}} *

    {{ title }}

    *
    {{{ excerpt }}}
    * {{/getPostsByTags}} * * IMPORTANT: It requires availability of the @website.contentStructure global variable */ function getPostsByTagsHelper (rendererInstance, Handlebars) { Handlebars.registerHelper('getPostsByTags', function (queryString, selectedTags, excludedPosts, options) { if (!rendererInstance.contentStructure.posts) { return 'Error: @website.contentStructure global variable is not available.'; } let count; let offset = 0; let allowedStatus = 'any'; let excludedStatus = ''; let orderby = false; let ordering = 'desc'; let customField = false; let compareLanguage = false; let tagAs = 'slug'; let operator = 'OR'; if (typeof queryString === 'string' && queryString.indexOf('=') > -1) { options = selectedTags; // have to override option with second argument as query string syntax uses only one argument queryString = queryString.split('&').map(pair => pair.split('=')); let queryStringData = {}; for (let i = 0; i < queryString.length; i++) { let key = queryString[i][0]; let value = queryString[i][1]; queryStringData[key] = value; } if (queryStringData['count']) { count = parseInt(queryStringData['count'], 10); if (count === -1) { count = 999; } } if (queryStringData['excluded']) { excludedPosts = queryStringData['excluded']; } if (queryStringData['excluded_status']) { excludedStatus = queryStringData['excluded_status']; } if (queryStringData['tags']) { selectedTags = queryStringData['tags']; } if (queryStringData['allowed']) { allowedStatus = queryStringData['allowed']; } if (queryStringData['offset']) { offset = parseInt(queryStringData['offset']); } if (queryStringData['orderby']) { orderby = queryStringData['orderby']; } if (queryStringData['ordering']) { ordering = queryStringData['ordering']; } if (queryStringData['customField']) { customField = queryStringData['customField']; } if (queryStringData['orderbyCompareLanguage']) { compareLanguage = queryStringData['orderbyCompareLanguage']; } if (queryStringData['tag_as']) { tagAs = queryStringData['tag_as']; } if (queryStringData['operator']) { operator = queryStringData['operator']; if (operator !== 'AND' && operator !== 'OR') { operator = 'OR'; } } } else { if (queryString === -1 || queryString === '-1') { count = 999; } else { count = parseInt(queryString, 10); } } let postsData; let content = ''; let filteredPosts = JSON.parse(JSON.stringify(rendererInstance.contentStructure.posts)); if (typeof excludedPosts === 'number' || (typeof excludedPosts === 'string' && excludedPosts !== '')) { if (typeof excludedPosts === 'number') { let excludedPost = excludedPosts; filteredPosts = filteredPosts.filter(post => post.id !== excludedPost) } else { excludedPosts = excludedPosts.split(',').map(n => parseInt(n, 10)); filteredPosts = filteredPosts.filter(post => excludedPosts.indexOf(post.id) === -1); } } if (allowedStatus !== 'any') { allowedStatus = allowedStatus.split(','); if (excludedStatus !== '') { excludedStatus = excludedStatus.split(','); } filteredPosts = filteredPosts.filter(post => { if (excludedStatus) { for (let i = 0; i < excludedStatus.length; i++) { if (post.status.indexOf(excludedStatus[i]) > -1) { return false; } } } for (let i = 0; i < allowedStatus.length; i++) { if (post.status.indexOf(allowedStatus[i]) > -1) { return true; } } return false; }); } else if (allowedStatus === 'any' && excludedStatus !== '') { excludedStatus = excludedStatus.split(','); filteredPosts = filteredPosts.filter(post => { for (let i = 0; i < excludedStatus.length; i++) { if (post.status.indexOf(excludedStatus[i]) > -1) { return false; } } return true; }); } if (typeof selectedTags === 'number') { let tagID = selectedTags; postsData = filteredPosts.filter(post => post.tags.filter(tag => tag.id === tagID).length || post.hiddenTags.filter(tag => tag.id === tagID).length); } else if (typeof selectedTags === 'string') { if (tagAs === 'slug') { let tagsSlugs = [...new Set(selectedTags.split(','))]; if (operator === 'OR') { postsData = filteredPosts.filter(post => post.tags.filter(tag => tagsSlugs.indexOf(tag.slug) > -1).length || post.hiddenTags.filter(tag => tagsSlugs.indexOf(tag.slug) > -1).length); } else if (operator === 'AND') { postsData = filteredPosts.filter(post => post.tags.filter(tag => tagsSlugs.indexOf(tag.slug) > -1).length + post.hiddenTags.filter(tag => tagsSlugs.indexOf(tag.slug) > -1).length === tagsSlugs.length); } } else { let tagsIDs = [...new Set(selectedTags.split(',').map(id => parseInt(id, 10)))]; if (operator === 'OR') { postsData = filteredPosts.filter(post => post.tags.filter(tag => tagsIDs.indexOf(tag.id) > -1).length || post.hiddenTags.filter(tag => tagsIDs.indexOf(tag.id) > -1).length); } else if (operator === 'AND') { postsData = filteredPosts.filter(post => post.tags.filter(tag => tagsIDs.indexOf(tag.id) > -1).length + post.hiddenTags.filter(tag => tagsIDs.indexOf(tag.id) > -1).length === tagsIDs.length); } } } else { postsData = filteredPosts; } if (orderby && ordering && ordering !== 'random') { postsData.sort((itemA, itemB) => { if (orderby === 'customField' && customField) { if (isNaN(itemA.postViewConfig[customField]) && isNaN(itemB.postViewConfig[customField])) { if (ordering === 'asc') { if (compareLanguage) { return itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField], compareLanguage); } else { return itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField]); } } else { if (compareLanguage) { return -(itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField], compareLanguage)); } else { return -(itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField])); } } } else { if (ordering === 'asc') { return parseInt(itemA.postViewConfig[customField], 10) - parseInt(itemB.postViewConfig[customField], 10); } else { return parseInt(itemB.postViewConfig[customField], 10) - parseInt(itemA.postViewConfig[customField], 10); } } } if (orderby !== 'customField') { if(typeof itemA[orderby] === 'string') { if (ordering === 'asc') { return itemA[orderby].localeCompare(itemB[orderby]); } else { return -(itemA[orderby].localeCompare(itemB[orderby])); } } else { if (ordering === 'asc') { return itemA[orderby] - itemB[orderby]; } else { return itemB[orderby] - itemA[orderby]; } } } }); } else if (ordering === 'random') { postsData.sort(() => 0.5 - Math.random()); } for (let i = offset; i < count + offset; i++) { if (postsData.length >= i + 1) { options.data.index = i; content += options.fn(postsData[i]); } else { break; } } if (content === '') { return ''; } return content; }); } module.exports = getPostsByTagsHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/get-posts.js ================================================ /** * Helper for loading posts data * * {{#getPosts "POST_ID_1,POST_ID_2,POST_ID_N" "prefix" "suffix"}} *
    *

    {{ title }}

    *
    {{{ excerpt }}}
    *
    * {{/getPosts}} * * Posts are ordered by the ID order in the string. * * The second parameter creates HTML prefix, the third parameter creates HTML suffix for the generated output. * * IMPORTANT: It requires availability of the @website.contentStructure global variable */ function getPostsHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('getPosts', function (postIDs, prefix, suffix, options) { if (!rendererInstance.contentStructure.posts) { return 'Error: @website.contentStructure global variable is not available.'; } let content = ''; postIDs = postIDs.split(',').map(n => parseInt(n, 10)); for (let i = 0; i < postIDs.length; i++) { let postData = rendererInstance.contentStructure.posts.filter(post => post.id === postIDs[i]); if (postData.length) { options.data.index = i; content += options.fn(postData[0]); } } if(content === '') { return ''; } content = [prefix, content, suffix].join(''); return content; }); } module.exports = getPostsHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/get-tag.js ================================================ /** * Helper for loading tag data * * {{#getTag TAG_ID}} *

    {{ name }}

    * {{/getTag}} * * {{#getTag "TAG_SLUG"}} *

    {{ name }}

    * {{/getTag}} * * IMPORTANT: It requires availability of the @website.contentStructure global variable */ function getTagHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('getTag', function (selectedTag, options) { if (!rendererInstance.contentStructure.tags) { return 'Error: @website.contentStructure global variable is not available.'; } let tagData; if (typeof selectedTag === 'number') { tagData = rendererInstance.contentStructure.tags.filter(tag => tag.id === selectedTag); } else { tagData = rendererInstance.contentStructure.tags.filter(tag => tag.slug === selectedTag); } if(!tagData.length) { return ''; } return options.fn(tagData[0]); }); } module.exports = getTagHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/get-tags.js ================================================ /** * Helper for loading tags data * * {{#getTags "TAG_ID_1,TAG_ID_2,TAG_ID_N" "prefix" "suffix"}} *
  • {{ name }}
  • * {{/getTags}} * * Tags are ordered by the ID order in the string. * * The second parameter creates HTML prefix, the third parameter creates HTML suffix for the generated output. * * IMPORTANT: It requires availability of the @website.contentStructure global variable */ function getTagsHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('getTags', function (tagsIDs, prefix, suffix, options) { if (!rendererInstance.contentStructure.tags) { return 'Error: @website.contentStructure global variable is not available.'; } let content = ''; tagsIDs = tagsIDs.split(',').map(n => parseInt(n, 10)); for (let i = 0; i < tagsIDs.length; i++) { let tagData = rendererInstance.contentStructure.tags.filter(tag => tag.id === tagsIDs[i]); if (tagData.length) { options.data.index = i; content += options.fn(tagData[0]); } } if(content === '') { return ''; } content = [prefix, content, suffix].join(''); return content; }); } module.exports = getTagsHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/image-dimensions.js ================================================ const Handlebars = require('handlebars'); const sizeOf = require('image-size'); const path = require('path'); const normalizePath = require('normalize-path'); /** * Helper for creating width/height attributes from the provided image * * {{imageDimensions @config.custom.imageOptionName}} * * @returns {string} - string with width and height attributes based on a given image */ function imageDimensionsHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('imageDimensions', function (url) { if (!url) { return ''; } url = normalizePath(url); let basicUrl = normalizePath(rendererInstance.siteConfig.domain); let basicDir = normalizePath(rendererInstance.inputDir); let imagePath = url.replace(basicUrl, ''); imagePath = path.join(basicDir, imagePath); let output = ''; try { let dimensions = sizeOf(imagePath); if(dimensions) { output = ' width="' + dimensions.width + '" height="' + dimensions.height + '" '; } } catch(e) { console.log('Image dimensions HSB helper: wrong image path - missing dimensions'); } return new Handlebars.SafeString(output); }); } module.exports = imageDimensionsHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/is-current-page.js ================================================ /** * Helper for checking if a given page number is a current page * * Useful in pagination * * {{#isCurrentPage @pagination.currentPage this}} * * @returns {callback} */ function isCurrentPage(current, iteration, options) { if (current === iteration) { return options.fn(this); } return options.inverse(this); } module.exports = isCurrentPage; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/is-empty.js ================================================ /** * Helper for checking if collection is empty * * {{#isEmpty value}} * ... * {{/isEmpty}} * * @returns {callback} */ function isEmpty(obj, options) { if(Object.keys(obj).length === 0) { return options.fn(this); } else { return options.inverse(this); } } module.exports = isEmpty; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/is-not-empty.js ================================================ /** * Helper for checking if collection is not empty * * {{#isNotEmpty value}} * ... * {{/isNotEmpty}} * * @returns {callback} * */ function isNotEmpty(obj, options) { if(Object.keys(obj).length === 0) { return options.inverse(this); } else { return options.fn(this); } } module.exports = isNotEmpty; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/is-not.js ================================================ /** * Helper for detecting contexts - opposite to #is helper * * {{#isNot 'index'}} * * {{#isNot 'tag,post'}} * * Available phrases: index, blogindex, tag, post, page, author, 404, search, pagination, index-pagination, tag-pagination, author-pagination * * @returns {callback} */ function isNot (conditional, options) { let contextIsCorrect = false; let contextsToCheck = conditional.split(','); contextsToCheck = contextsToCheck.map(context => context.trim()); for (let context of contextsToCheck) { if (options.data.context.indexOf(context) > -1) { contextIsCorrect = true; break; } } if (!contextIsCorrect) { return options.fn(this); } else { return options.inverse(this); } } module.exports = isNot; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/is.js ================================================ /** * Helper for detecting contexts * * {{#is 'index'}} * * {{#is 'tag,post'}} * * Available phrases: index, blogindex, tag, post, page, author, 404, search, pagination, index-pagination, tag-pagination, author-pagination * * @returns {callback} */ function is(conditional, options) { let contextIsCorrect = false; let contextsToCheck = conditional.split(','); contextsToCheck = contextsToCheck.map(context => context.trim()); for (let context of contextsToCheck) { if (options.data.context.indexOf(context) > -1) { contextIsCorrect = true; break; } } if (contextIsCorrect) { return options.fn(this); } else { return options.inverse(this); } } module.exports = is; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/join.js ================================================ const Handlebars = require('handlebars'); /** * Helper for concatenating values with a specific separator * * {{join ',' 'a' 1 'b'}} * * @returns {callback} */ function join () { let inputs = Array.from(arguments); let separator = inputs.shift(); inputs.pop(); return inputs.join(separator); } module.exports = join; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/js.js ================================================ const fs = require('fs'); const FileHelper = require('./../../../../helpers/file.js'); const crypto = require('crypto'); const path = require('path'); const memoize = require('fast-memoize'); /** * * Helper function used to calculate MD5 sum of the given file contents * * @param {string} localPath - path to the file * @param {string} overridedLocalPath - path to the overrided version of the file * * @returns {string} - MD5 sum based on the given file contents */ function getMD5(localPath, overridedLocalPath) { let fileContent = ''; if (fs.existsSync(overridedLocalPath)) { fileContent = FileHelper.readFileSync(overridedLocalPath); } else { fileContent = FileHelper.readFileSync(localPath); } return crypto.createHash('md5').update(fileContent).digest('hex'); } const memoizedMD5 = memoize(getMD5); /** * Helper for loading JS files from the assets directory * * It also adds MD5 sum hash as a v= param for preventing browser cache * * {{js "filepath.js"}} * * @returns {string} - path to the JS file with v= param based on the MD5 sume of the given file */ function JSHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('js', function (filename) { let md5Sum = ''; let localPath = path.join(rendererInstance.inputDir, 'themes', rendererInstance.themeConfig.name.toLowerCase(), rendererInstance.themeConfig.files.assetsPath, 'js', filename); let overridedLocalPath = path.join(rendererInstance.inputDir, 'themes', rendererInstance.themeConfig.name.toLowerCase() + '-override', rendererInstance.themeConfig.files.assetsPath, 'js', filename); let versionSuffix = ''; if (rendererInstance.siteConfig.advanced.versionSuffix) { md5Sum = memoizedMD5(localPath, overridedLocalPath); versionSuffix = '?v=' + md5Sum; } let url = [ rendererInstance.siteConfig.domain, rendererInstance.themeConfig.files.assetsPath, 'js', filename + versionSuffix ].join('/'); return new Handlebars.SafeString(url); }); } module.exports = JSHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/json-ld.js ================================================ const Handlebars = require('handlebars'); const moment = require('moment'); const sizeOf = require('image-size'); const path = require('path'); const URLHelper = require('../../helpers/url'); /** * Helper for creating JSON-LD data * * {{jsonLD}} * * @returns {string} '; moment.locale(momentOriginalLocale); return new Handlebars.SafeString(output); }); } module.exports = jsonLDHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/jsonify.js ================================================ /** * Helper for creating JSON-safe strings * * {{{jsonify string|object}}} * * @returns {string} string prepared for use in JSON */ function jsonify(content) { return JSON.stringify(content); } module.exports = jsonify; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/lazyload.js ================================================ const Handlebars = require('handlebars'); /** * Helper for creating loading attributes * * {{ lazyload eager }} * * @returns {string} loading attribute with a value specified as a param */ function lazyloadHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('lazyload', function(value) { let output = ''; if (rendererInstance.siteConfig.advanced.mediaLazyLoad && ['eager', 'lazy', 'auto'].indexOf(value) > -1) { output = ' loading="' + value + '"'; } return new Handlebars.SafeString(output); }); } module.exports = lazyloadHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/math.js ================================================ /** * Helper for doing a simple math operations * * {{math @index '+' 1}} * * {{math @index '-' 1}} * * Available phrases operations: +, -, *, /, % * * @returns {number} - value based on the operation result */ function math(a, operator, b) { a = parseFloat(a); b = parseFloat(b); return { "+": a + b, "-": a - b, "*": a * b, "/": a / b, "%": a % b }[operator]; } module.exports = math; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/menu-item-classes.js ================================================ const Handlebars = require('handlebars'); /** * Helpers for creating CSS classes in menu * * {{menuItemClasses}} * {{menuItemClasses "active"}} * {{menuItemClasses "active" "active-parent"}} * {{menuItemClasses "active" "active-parent" "has-submenu"}} * * {{menuItemClassesRaw}} * {{menuItemClassesRaw "active"}} * {{menuItemClassesRaw "active" "active-parent"}} * {{menuItemClassesRaw "active" "active-parent" "has-submenu"}} * * @returns {string} CSS class names for the given menu item */ function menuItemClassesHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('menuItemClasses', function(activeClass, activeParentClass, hasSubmenuClass) { let context = rendererInstance.menuContext; let output = []; // If there is no arguments Handlebars will push // context as an argument which will cause // that the default params won't work if (typeof activeClass !== 'string') { activeClass = 'active'; } if (typeof activeParentClass !== 'string') { activeParentClass = 'active-parent'; } if (typeof hasSubmenuClass !== 'string') { hasSubmenuClass = 'has-submenu'; } // Check for the state of the menu item if (hasActiveChild(this.items, context)) { output.push(activeParentClass); } else if ( (this.type === 'blogpage' && context[0] === 'blogpage') || (this.type === 'frontpage' && context[0] === 'frontpage') || (this.type === 'frontpage' && context[0] === 'page' && context[2] && context[2] === 'page-' + rendererInstance.siteConfig.advanced.pageAsFrontpage) || (this.type === 'tags' && context[0] === 'tags') || (this.type === context[0] && this.link === context[1]) ) { output.push(activeClass); } if (this.cssClass !== '') { output.push(this.cssClass); } if (this.items.length) { output.push(hasSubmenuClass); } // Prepare output if (output.length) { output = ' class="' + output.join(' ') + '"'; return new Handlebars.SafeString(output); } return ''; }); Handlebars.registerHelper('menuItemClassesRaw', function(activeClass, activeParentClass, hasSubmenuClass) { let context = rendererInstance.menuContext; let output = []; // If there is no arguments Handlebars will push // context as an argument which will cause // that the default params won't work if (typeof activeClass !== 'string') { activeClass = 'active'; } if (typeof activeParentClass !== 'string') { activeParentClass = 'active-parent'; } if (typeof hasSubmenuClass !== 'string') { hasSubmenuClass = 'has-submenu'; } // Check for the state of the menu item if (hasActiveChild(this.items, context)) { output.push(activeParentClass); } else if ( (this.type === 'frontpage' && context[0] === 'frontpage') || (this.type === 'tags' && context[0] === 'tags') || (this.type === context[0] && this.link === context[1]) ) { output.push(activeClass); } if (this.cssClass !== '') { output.push(this.cssClass); } if (this.items.length) { output.push(hasSubmenuClass); } // Prepare output if (output.length) { output = output.join(' '); return new Handlebars.SafeString(output); } return ''; }); } /** * Private function for finding the active * childrens (for the adding active-parent CSS class purpose) * * @param {object} items - items from the menu * @param {string} context - name of the context * * @returns {boolean} - true if the given submenu has an active menu item, otherwise false */ function hasActiveChild(items, context) { let result = false; for (let i = 0; i < items.length; i++) { if ( (items[i].type === 'frontpage' && context[0] === 'frontpage') || (items[i].type === 'tags' && context[0] === 'tags') || (items[i].type === context[0] && items[i].link === context[1]) ) { return true; } if (items[i].items.length) { result = hasActiveChild(items[i].items, context); if (result) { return true; } } } return result; } module.exports = menuItemClassesHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/menu-url.js ================================================ const Handlebars = require('handlebars'); const slug = require('./../../../../helpers/slug'); /** * Helper for creating URLs in menu * * {{menuUrl}} * * Available types: * - post * - page * - tag * - frontpage * - blogpage * - tags * - external * - internal * * @returns {string} - URL for the current menu item */ function menuURLHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('menuUrl', function() { let output = ''; let baseUrl = rendererInstance.siteConfig.domain; // Link to the single post pages if (this.type === 'post') { if (rendererInstance.siteConfig.advanced.urls.cleanUrls) { if (rendererInstance.siteConfig.advanced.urls.postsPrefix) { output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.postsPrefix + '/' + this.link + '/'; } else { output = baseUrl + '/' + this.link + '/'; } // In the preview mode we have to load URLs with // index.html as filesystem on OS doesn't behave // as the server environment and not redirect to // a proper URL if(rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) { output += 'index.html'; } } else { if (rendererInstance.siteConfig.advanced.urls.postsPrefix) { output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.postsPrefix + '/' + this.link + '.html'; } else { output = baseUrl + '/' + this.link + '.html'; } } } // Link to the single page if (this.type === 'page') { let parentItems = rendererInstance.cachedItems.pagesStructureHierarchy[this.linkID]; let pageSlug = this.link; if (rendererInstance.siteConfig.advanced.usePageAsFrontpage && rendererInstance.siteConfig.advanced.pageAsFrontpage === this.linkID) { output = baseUrl + '/'; // In the preview mode we have to load URLs with // index.html as filesystem on OS doesn't behave // as the server environment and not redirect to // a proper URL if (rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) { output += 'index.html'; } } else { if (rendererInstance.siteConfig.advanced.urls.cleanUrls && parentItems && parentItems.length) { let slugs = []; for (let i = 0; i < parentItems.length; i++) { if (rendererInstance.cachedItems.pages[parentItems[i]]) { slugs.push(rendererInstance.cachedItems.pages[parentItems[i]].slug); } } slugs.push(this.link); pageSlug = slugs.join('/'); } if (rendererInstance.siteConfig.advanced.urls.cleanUrls) { output = baseUrl + '/' + pageSlug + '/'; // In the preview mode we have to load URLs with // index.html as filesystem on OS doesn't behave // as the server environment and not redirect to // a proper URL if (rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) { output += 'index.html'; } } else { output = baseUrl + '/' + pageSlug + '.html'; } } } // Link to the tag pages if (this.type === 'tag') { output = baseUrl + '/' + this.link + '/'; if (rendererInstance.siteConfig.advanced.urls.tagsPrefix !== '') { output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.tagsPrefix + '/' + this.link + '/'; } if (rendererInstance.siteConfig.advanced.urls.postsPrefix && rendererInstance.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) { output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.postsPrefix + '/' + rendererInstance.siteConfig.advanced.urls.tagsPrefix + '/' + this.link + '/'; } // In the preview mode we have to load URLs with // index.html as filesystem on OS doesn't behave // as the server environment and not redirect to // a proper URL if (rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) { output += 'index.html'; } } // Link to the author pages if (this.type === 'author') { output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.authorsPrefix + '/' + slug(this.link) + '/'; if (rendererInstance.siteConfig.advanced.urls.postsPrefix && rendererInstance.siteConfig.advanced.urls.authorsPrefixAfterPostsPrefix) { output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.postsPrefix + '/' + rendererInstance.siteConfig.advanced.urls.authorsPrefix + '/' + slug(this.link) + '/'; } // In the preview mode we have to load URLs with // index.html as filesystem on OS doesn't behave // as the server environment and not redirect to // a proper URL if (rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) { output += 'index.html'; } } // Link to the frontpage - just the page domain name if (this.type === 'frontpage') { output = baseUrl + '/'; // In the preview mode we have to load URLs with // index.html as filesystem on OS doesn't behave // as the server environment and not redirect to // a proper URL if (rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) { output += 'index.html'; } } // Link to the blogpage - just the page domain name or page with posts prefix if (this.type === 'blogpage') { output = baseUrl + '/'; if (rendererInstance.siteConfig.advanced.urls.postsPrefix) { output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.postsPrefix + '/'; } // In the preview mode we have to load URLs with // index.html as filesystem on OS doesn't behave // as the server environment and not redirect to // a proper URL if (rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) { output += 'index.html'; } } // Link to the tags list - just the page domain name with tags prefix if (this.type === 'tags') { output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.tagsPrefix + '/'; if (rendererInstance.siteConfig.advanced.urls.postsPrefix && rendererInstance.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) { output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.postsPrefix + '/' + rendererInstance.siteConfig.advanced.urls.tagsPrefix + '/'; } // In the preview mode we have to load URLs with // index.html as filesystem on OS doesn't behave // as the server environment and not redirect to // a proper URL if (rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) { output += 'index.html'; } } // External links which should start with protocol if (this.type === 'external') { output = this.link; } // Internal links which should start with the page domain name if (this.type === 'internal') { output = baseUrl + '/' + this.link; } output = Handlebars.Utils.escapeExpression(output); return new Handlebars.SafeString(output); }); } module.exports = menuURLHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/meta-description.js ================================================ const Handlebars = require('handlebars'); /** * Helper for generating meta_description * * {{metaDescription}} * * @returns {string} - tag with meta description */ function metaDescription(options) { if (options.data.root.metaDescriptionRaw !== '') { if (options.data.root.metaRobotsRaw.indexOf('noindex') !== -1) { return ''; } let output = ''; return new Handlebars.SafeString(output); } return ''; } module.exports = metaDescription; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/meta-robots.js ================================================ const Handlebars = require('handlebars'); /** * Helper for generating meta_robots * * {{metaRobots}} * * @returns {string} tag with the meta robots value */ function metaRobotsHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('metaRobots', function (options) { // If canonical is set - skip meta robots tag if (options.data.root.hasCustomCanonicalUrl) { return ''; } if ( Array.isArray(options.data.context) && options.data.context[0] && ( ( rendererInstance.siteConfig.advanced.homepageNoIndexPagination && options.data.context.indexOf('index-pagination') !== -1 ) || ( rendererInstance.siteConfig.advanced.tagNoIndexPagination && options.data.context.indexOf('tag-pagination') !== -1 ) || ( rendererInstance.siteConfig.advanced.authorNoIndexPagination && options.data.context.indexOf('author-pagination') !== -1 ) ) ) { return new Handlebars.SafeString(''); } if (options.data.root.metaRobotsRaw === 'index, follow') { return ''; } if (options.data.root.metaRobotsRaw !== '') { return new Handlebars.SafeString(''); } return ''; }); } module.exports = metaRobotsHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/not-contains.js ================================================ /** * Helper for checking if a specifc value is not inside comma-separated string * * {{#notContains 'abc' 'abc,def'}} * * @returns {callback} */ function notContains (needle, haystack, options) { if (needle === undefined || haystack === undefined) { return; } if (typeof haystack === 'object' && haystack.string) { haystack = haystack.string; } haystack = haystack.split(','); if (typeof needle === 'number') { haystack = haystack.map(n => parseInt(n, 10)); } if (haystack.indexOf(needle) === -1) { return options.fn(this); } return options.inverse(this) } module.exports = notContains; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/orderby.js ================================================ /** * Helper used to order collection * * @param collection * @param field * @param direction * @param langForLocaleCompare */ function orderby (collection, field, direction, langForLocaleCompare = false) { collection.sort((itemA, itemB) => { if (typeof itemA[field] === 'string') { if (langForLocaleCompare) { if (direction === 'ASC') { return itemA[field].localeCompare(itemB[field], langForLocaleCompare); } else { return -1 * itemA[field].localeCompare(itemB[field], langForLocaleCompare); } } if (direction === 'ASC') { return itemA[field].localeCompare(itemB[field]); } else { return -1 * itemA[field].localeCompare(itemB[field]); } } else { if (direction === 'ASC') { return itemA[field] - itemB[field]; } else { return itemB[field] - itemA[field]; } } }); } module.exports = orderby; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/page-url.js ================================================ /** * Helper for creating pagination URL * * {{pageUrl @pagination.context NUMBER}} * * @returns {string} URL for the specific page in the pagination for the given context */ function pageURLHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('pageUrl', function (context, number) { let path = [rendererInstance.siteConfig.domain]; number = parseInt(number, 10); // Skip context for homepage if (context !== '') { path.push(context); } if (context === '' && rendererInstance.siteConfig.advanced.urls.postsPrefix) { path.push(rendererInstance.siteConfig.advanced.urls.postsPrefix); } // Skip page/X for URLs in page = 1 if (number > 1) { path.push(rendererInstance.siteConfig.advanced.urls.pageName); path.push(number); } if(rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) { path.push('index.html'); } // Connect the URL parts path = path.join('/'); // Add trailing slash only if adding index.html is disabled and there is no preview mode active if (!rendererInstance.previewMode && !rendererInstance.siteConfig.advanced.urls.addIndex) { path += '/'; } let url = Handlebars.Utils.escapeExpression(path); return new Handlebars.SafeString(url); }); } module.exports = pageURLHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/publii-footer.js ================================================ const Handlebars = require('handlebars'); const Gdpr = require('./../../helpers/gdpr.js'); /** * Helper for creating additional useful tags * * {{{ publiiFooter }}} * * @returns {string} content of the Publii-specific element like GDPR popup */ function publiiFooterHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('publiiFooter', function (context) { let output = ''; if (rendererInstance.plugins.hasInsertions('publiiFooter')) { output += "\n"; output += rendererInstance.plugins.runInsertions('publiiFooter', rendererInstance); } if (rendererInstance.siteConfig.advanced.gdpr.enabled) { output += Gdpr.popupHtmlOutput(rendererInstance.siteConfig.advanced.gdpr, rendererInstance); output += Gdpr.popupJsOutput(rendererInstance.siteConfig.advanced.gdpr); } return new Handlebars.SafeString(output); }); } module.exports = publiiFooterHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/publii-head.js ================================================ const Handlebars = require('handlebars'); /** * Helper for creating additional useful tags * * {{publiiHead}} * * @returns {string} content of the Publii-specific meta tags */ function publiiHeadHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('publiiHead', function (context) { let output = ''; if ( rendererInstance.themeConfig.supportedFeatures && rendererInstance.themeConfig.supportedFeatures.embedConsents && rendererInstance.siteConfig.advanced.gdpr.enabled && rendererInstance.siteConfig.advanced.gdpr.allowAdvancedConfiguration && rendererInstance.siteConfig.advanced.gdpr.embedConsents && rendererInstance.siteConfig.advanced.gdpr.embedConsents.length ) { let configRevision = ''; let configTTL = 0; if (rendererInstance.siteConfig.advanced.gdpr.cookieSettingsRevision) { configRevision = '-v' + parseInt(rendererInstance.siteConfig.advanced.gdpr.cookieSettingsRevision, 10); } if (rendererInstance.siteConfig.advanced.gdpr.cookieSettingsTTL) { configTTL = parseInt(rendererInstance.siteConfig.advanced.gdpr.cookieSettingsTTL, 10); } output += ` `; } if (rendererInstance.plugins.hasInsertions('publiiHead')) { output += "\n"; output += rendererInstance.plugins.runInsertions('publiiHead', rendererInstance); } return new Handlebars.SafeString(output); }); } module.exports = publiiHeadHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/responsive-image-attributes.js ================================================ const Handlebars = require('handlebars'); const responsiveSrcSet = require('./responsive-srcset.js'); const responsiveSizes = require('./responsive-sizes.js'); /** * Helper for sizes attribute for the images from options * * {{responsiveImageAttributes @config.custom.imageOptionName [type] [group]}} * * {{responsiveImageAttributes 'featuredImage' srcset.post sizes.post}} * {{responsiveImageAttributes 'tagImage' srcset.post sizes.post}} * {{responsiveImageAttributes 'authorImage' srcset.post sizes.post}} * * @returns {string} - string with the srcset and sizes attributes */ function responsiveImageAttributesHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('responsiveImageAttributes', function (firstParam, secondParam, thirdParam) { if (!firstParam) { return ''; } if ( firstParam === 'featuredImage' || firstParam === 'tagImage' || firstParam === 'authorImage' ) { if (secondParam && thirdParam) { return new Handlebars.SafeString('srcset="' + secondParam + '" sizes="' + thirdParam + '"'); } return ''; } let srcSet = responsiveSrcSet.returnSrcSetAttribute.bind(rendererInstance)(firstParam, secondParam, thirdParam); if (typeof secondParam !== 'string') { if (firstParam.indexOf('/media/authors/') > -1) { secondParam = 'authorImages'; } else if (firstParam.indexOf('/media/tags/') > -1) { secondParam = 'tagImages'; } else if (firstParam.indexOf('/media/posts/') > -1 || firstParam.indexOf('/media/pages/') > -1) { secondParam = 'contentImages'; } else if (firstParam.indexOf('/media/website/') > -1) { secondParam = 'optionImages'; } } let sizes = responsiveSizes.returnSizesAttribute.bind(rendererInstance)(secondParam, thirdParam); if (srcSet) { return new Handlebars.SafeString(srcSet + ' ' + sizes); } return new Handlebars.SafeString(srcSet); }); } module.exports = responsiveImageAttributesHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/responsive-sizes.js ================================================ const Handlebars = require('handlebars'); const UtilsHelper = require('./../../../../helpers/utils.js'); /** * Helper for sizes attribute for the images from options * * {{responsiveSizes type [group]}} * * @returns {string} - string with the sizes attribute */ function responsiveSizesHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('responsiveSizes', returnSizesAttribute.bind(rendererInstance)); } function returnSizesAttribute (type, group) { if (!UtilsHelper.responsiveImagesConfigExists(this.themeConfig)) { return ''; } let output = ''; let responsiveConfig = this.themeConfig.files.responsiveImages; let useType = false; let useGroup = false; if (typeof type === "string") { useType = true; } if (typeof group === "string") { useGroup = true; } if (!useType) { return ''; } if (useGroup && responsiveConfig[type] && responsiveConfig[type].sizes && responsiveConfig[type].sizes[group]) { output = ' sizes="' + responsiveConfig[type].sizes[group] + '" '; } else if (!useGroup && responsiveConfig[type] && responsiveConfig[type].sizes) { output = ' sizes="' + responsiveConfig[type].sizes + '" '; } return new Handlebars.SafeString(output); } module.exports = { responsiveSizesHelper, returnSizesAttribute }; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/responsive-srcset.js ================================================ const Handlebars = require('handlebars'); const path = require('path'); const normalizePath = require('normalize-path'); const UtilsHelper = require('./../../../../helpers/utils.js'); const URLHelper = require('./../../helpers/url.js'); /** * Helper for srcset attribute from the provided image * * {{responsiveSrcSet @config.custom.imageOptionName [type] [group]}} * * @returns {string} - string with the srcset attribute */ function responsiveSrcSetHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('responsiveSrcSet', returnSrcSetAttribute.bind(rendererInstance)); } function returnSrcSetAttribute (url, type, group) { if (!url) { return; } url = URLHelper.fixProtocols(normalizePath(url)); let output = ''; let dimensions = false; let dimensionsData = false; if (!UtilsHelper.responsiveImagesConfigExists(this.themeConfig)) { return output; } // skip GIF and SVG images if (url.slice(-4) === '.gif' || url.slice(-4) === '.svg') { return output; } if (typeof type !== "string") { type = false; } if (typeof group !== "string") { group = false; } if (!type) { if (url.indexOf('/media/authors/') > -1) { type = 'authorImages'; } else if (url.indexOf('/media/tags/') > -1) { type = 'tagImages'; } else if (url.indexOf('/media/posts/') > -1 || url.indexOf('/media/pages/') > -1) { type = 'contentImages'; } else if (url.indexOf('/media/website/') > -1) { type = 'optionImages'; } } if (UtilsHelper.responsiveImagesConfigExists(this.themeConfig, type)) { dimensions = UtilsHelper.responsiveImagesDimensions(this.themeConfig, type, group); dimensionsData = UtilsHelper.responsiveImagesData(this.themeConfig, type, group); } if (!dimensions) { return; } let srcset = []; for(let name of dimensions) { let filename = url.split('/'); filename = filename[filename.length-1]; let filenameFile = path.parse(filename).name; let filenameExtension = path.parse(filename).ext; let useWebp = false; if (this.siteConfig?.advanced?.forceWebp) { useWebp = true; } if (useWebp) { filenameExtension = '.webp'; } let baseUrlWithoutFilename = url.replace(filename, ''); let responsiveImage = baseUrlWithoutFilename + 'responsive/' + filenameFile + '-' + name + filenameExtension; srcset.push(responsiveImage + ' ' + dimensionsData[name].width + 'w'); } output = ' srcset="' + srcset.join(' ,') + '" '; return new Handlebars.SafeString(output); } module.exports = { responsiveSrcSetHelper, returnSrcSetAttribute }; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/reverse.js ================================================ /** * Helper used to reverse collection * * @param collection */ function reverse (collection) { collection.reverse(); } module.exports = reverse; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/social-meta-tags.js ================================================ const stripTags = require('striptags'); const sizeOf = require('image-size'); const path = require('path'); const normalizePath = require('normalize-path'); /** * Helper for creating Open Graph and Twitter Cars metatags * * {{socialMetaTags}} * * @returns {string} - meta tags for Open Graph and Twitter */ function socialMetaTagsHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('socialMetaTags', function (contextData) { if (rendererInstance.siteConfig.deployment.relativeUrls) { return new Handlebars.SafeString(''); } let output = ''; let openGraphEnabled = rendererInstance.siteConfig.advanced.openGraphEnabled; let openGraphImage = rendererInstance.siteConfig.advanced.openGraphImage; let siteName = contextData.data.website.name; let image = ''; let title = ''; let description = ''; let openGraphType = 'website'; let twitterUsername = rendererInstance.siteConfig.advanced.twitterUsername; let twitterCardsType = rendererInstance.siteConfig.advanced.twitterCardsType; let twitterCardsEnabled = rendererInstance.siteConfig.advanced.twitterCardsEnabled; // Get SEO title if exists if( rendererInstance.siteConfig.advanced.metaTitle && rendererInstance.siteConfig.advanced.metaTitle != '' ) { let siteNameValue = rendererInstance.siteConfig.name; if(rendererInstance.siteConfig.displayName) { siteNameValue = rendererInstance.siteConfig.displayName; } siteName = rendererInstance.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteNameValue); } if ( !rendererInstance.siteConfig.advanced.usePageAsFrontpage && rendererInstance.siteConfig.advanced.urls.postsPrefix !== '' && contextData.data.context.indexOf('index') > -1 && rendererInstance.siteConfig.advanced.homepageMetaTitle ) { let siteNameValue = rendererInstance.siteConfig.name; if (rendererInstance.siteConfig.displayName) { siteNameValue = rendererInstance.siteConfig.displayName; } siteName = rendererInstance.siteConfig.advanced.homepageMetaTitle.replace(/%sitename/g, siteNameValue); } if(contextData.data.context.indexOf('post') === -1 && contextData.data.context.indexOf('page') === -1) { // Get tag values according to the current context - listing or single post page // Data for the index/tag listing page image = contextData.data.website.logo; title = siteName; description = contextData.data.root.metaDescriptionRaw; if (contextData.data.context.indexOf('tag') !== -1) { title = contextData.data.root.tag.name; if (rendererInstance.siteConfig.advanced.usePageTitleInsteadItemName) { title = contextData.data.root.title; } } if (contextData.data.context.indexOf('author') !== -1) { title = contextData.data.root.author.name; if (rendererInstance.siteConfig.advanced.usePageTitleInsteadItemName) { title = contextData.data.root.title; } } } else { // Data for the single post or page let itemData = contextData.data.root.post; if (!itemData) { itemData = contextData.data.root.page; } image = itemData.featuredImage.url; openGraphType = 'article'; if(!image) { image = contextData.data.website.logo; } title = itemData.title; if (rendererInstance.siteConfig.advanced.usePageTitleInsteadItemName) { title = contextData.data.root.title; } description = contextData.data.root.metaDescriptionRaw; if(description === '') { description = itemData.excerpt; } } // Get fallback image if available if (openGraphImage && openGraphImage !== '' && image === contextData.data.website.logo) { image = openGraphImage; } // Generate Open Graph tags if (openGraphEnabled) { output += ''; if (image) { output += ''; } let ogImageDimensions = false; let imageLocalPath = image; try { let baseLocalPath = path.join(rendererInstance.sitesDir, rendererInstance.siteName, 'input', 'media'); baseLocalPath = normalizePath(baseLocalPath); imageLocalPath = normalizePath(image) imageLocalPath = imageLocalPath.split('/media/'); if (imageLocalPath[1]) { imageLocalPath = path.join(baseLocalPath, imageLocalPath[1]); ogImageDimensions = sizeOf(imageLocalPath); } } catch(e) { console.log('OG image - wrong image path - missing dimensions', imageLocalPath); ogImageDimensions = false; } if (ogImageDimensions) { output += ''; output += '' } output += ''; output += ''; output += ''; output += ''; if (rendererInstance.siteConfig.advanced.openGraphAppId !== '') { output += ''; } } // If user set Twitter username - generate Twitter Cards tags if(twitterCardsEnabled && twitterUsername && twitterUsername !== '') { if(twitterUsername.indexOf('@') !== 0) { twitterUsername = '@' + twitterUsername; } output += ''; output += ''; output += ''; output += ''; if(image) { output += ''; } } if (rendererInstance.plugins.hasModifiers('socialMetaTags')) { output = rendererInstance.plugins.runModifiers('socialMetaTags', rendererInstance, output); } return new Handlebars.SafeString(output); }); } module.exports = socialMetaTagsHelper; ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/specs/check-if-all.spec.js ================================================ const assert = require('assert'); const checkIfAll = require('../check-if-all.js'); describe('Handlebars - checkIfAll block helper', function() { // Below object emulates Handlebars behaviour let optionsParam = { fn: () => true, inverse: () => false }; it('should return false when there is no arguments', function() { assert.equal(false, checkIfAll(optionsParam)); }); it('should return false for falsy values arguments', function() { assert.equal(false, checkIfAll('', optionsParam)); assert.equal(false, checkIfAll("", optionsParam)); assert.equal(false, checkIfAll(NaN, optionsParam)); assert.equal(false, checkIfAll(null, optionsParam)); assert.equal(false, checkIfAll(undefined, optionsParam)); assert.equal(false, checkIfAll(0, optionsParam)); assert.equal(false, checkIfAll(false, optionsParam)); }); it('should return true if all arguments are true', function() { assert.equal(true, checkIfAll('a', optionsParam)); assert.equal(true, checkIfAll('a', 'b', optionsParam)); assert.equal(true, checkIfAll('a', 'b', 'c', optionsParam)); assert.equal(true, checkIfAll('a', 'b', 'c', 'd', optionsParam)); assert.equal(true, checkIfAll('a', 1, 10, true, optionsParam)); assert.equal(true, checkIfAll('lorem', 1, 'ipsum', [1], {a: 1}, optionsParam)); }); it('should return proper false when at least argument is false', function() { assert.equal(false, checkIfAll('a', false, optionsParam)); assert.equal(false, checkIfAll(false, 1, true, optionsParam)); assert.equal(false, checkIfAll(true, false, true, optionsParam)); assert.equal(false, checkIfAll('lorem', [1], false, optionsParam)); }); it('should return false when all arguments are false or falsy values', function() { assert.equal(false, checkIfAll(false, optionsParam)); assert.equal(false, checkIfAll(false, false, optionsParam)); assert.equal(false, checkIfAll(false, false, false, optionsParam)); assert.equal(false, checkIfAll(false, undefined, optionsParam)); assert.equal(false, checkIfAll(false, null, 0, optionsParam)); assert.equal(false, checkIfAll('', "", NaN, null, undefined, 0, false, optionsParam)); }); }); ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/specs/check-if-any.spec.js ================================================ const assert = require('assert'); const checkIfAny = require('../check-if-any.js'); describe('Handlebars - checkIfAny block helper', function() { // Below object emulates Handlebars behaviour let optionsParam = { fn: () => true, inverse: () => false }; it('should return false when there is no arguments', function() { assert.equal(false, checkIfAny(optionsParam)); }); it('should return false for falsy values arguments', function() { assert.equal(false, checkIfAny('', optionsParam)); assert.equal(false, checkIfAny("", optionsParam)); assert.equal(false, checkIfAny(NaN, optionsParam)); assert.equal(false, checkIfAny(null, optionsParam)); assert.equal(false, checkIfAny(undefined, optionsParam)); assert.equal(false, checkIfAny(0, optionsParam)); assert.equal(false, checkIfAny(false, optionsParam)); }); it('should return true if all arguments are true', function() { assert.equal(true, checkIfAny('a', optionsParam)); assert.equal(true, checkIfAny('a', 'b', optionsParam)); assert.equal(true, checkIfAny('a', 'b', 'c', optionsParam)); assert.equal(true, checkIfAny('a', 'b', 'c', 'd', optionsParam)); assert.equal(true, checkIfAny('a', 1, 10, true, optionsParam)); assert.equal(true, checkIfAny('lorem', 1, 'ipsum', [1], {a: 1}, optionsParam)); }); it('should return true when at least argument is true', function() { assert.equal(true, checkIfAny('a', false, optionsParam)); assert.equal(true, checkIfAny(false, 1, true, optionsParam)); assert.equal(true, checkIfAny(true, false, true, optionsParam)); assert.equal(true, checkIfAny('lorem', [1], false, optionsParam)); }); it('should return false when all arguments are false or falsy values', function() { assert.equal(false, checkIfAny(false, optionsParam)); assert.equal(false, checkIfAny(false, false, optionsParam)); assert.equal(false, checkIfAny(false, false, false, optionsParam)); assert.equal(false, checkIfAny(false, undefined, optionsParam)); assert.equal(false, checkIfAny(false, null, 0, optionsParam)); assert.equal(false, checkIfAny('', "", NaN, null, undefined, 0, false, optionsParam)); }); }); ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/specs/check-if-none.spec.js ================================================ const assert = require('assert'); const checkIfNone = require('../check-if-none.js'); describe('Handlebars - checkIfNone block helper', function() { // Below object emulates Handlebars behaviour let optionsParam = { fn: () => true, inverse: () => false }; it('should return true when there is no arguments', function() { assert.equal(true, checkIfNone(optionsParam)); }); it('should return true for falsy values arguments', function() { assert.equal(true, checkIfNone('', optionsParam)); assert.equal(true, checkIfNone("", optionsParam)); assert.equal(true, checkIfNone(NaN, optionsParam)); assert.equal(true, checkIfNone(null, optionsParam)); assert.equal(true, checkIfNone(undefined, optionsParam)); assert.equal(true, checkIfNone(0, optionsParam)); assert.equal(true, checkIfNone(false, optionsParam)); }); it('should return false if all arguments are true', function() { assert.equal(false, checkIfNone('a', optionsParam)); assert.equal(false, checkIfNone('a', 'b', optionsParam)); assert.equal(false, checkIfNone('a', 'b', 'c', optionsParam)); assert.equal(false, checkIfNone('a', 'b', 'c', 'd', optionsParam)); assert.equal(false, checkIfNone('a', 1, 10, true, optionsParam)); assert.equal(false, checkIfNone('lorem', 1, 'ipsum', [1], {a: 1}, optionsParam)); }); it('should return false when at least argument is true', function() { assert.equal(false, checkIfNone('a', false, optionsParam)); assert.equal(false, checkIfNone(false, 1, true, optionsParam)); assert.equal(false, checkIfNone(true, false, true, optionsParam)); assert.equal(false, checkIfNone('lorem', [1], false, optionsParam)); }); it('should return true when all arguments are false or falsy values', function() { assert.equal(true, checkIfNone(false, optionsParam)); assert.equal(true, checkIfNone(false, false, optionsParam)); assert.equal(true, checkIfNone(false, false, false, optionsParam)); assert.equal(true, checkIfNone(false, undefined, optionsParam)); assert.equal(true, checkIfNone(false, null, 0, optionsParam)); assert.equal(true, checkIfNone('', "", NaN, null, undefined, 0, false, optionsParam)); }); }); ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/specs/check-if.spec.js ================================================ const assert = require('assert'); const checkIf = require('../check-if.js'); describe('Handlebars - checkIf block helper', function() { // Below object emulates Handlebars behaviour let optionsParam = { fn: () => true, inverse: () => false }; it('should return false for too few arguments', function() { assert.equal(undefined, checkIf('a')); assert.equal(undefined, checkIf('a', '==')); }); it('should return false for non-existing operator', function() { assert.equal(undefined, checkIf('a', 'LOL', 'b')); }); it('should return proper value for the `==` operator', function() { assert.equal(true, checkIf('a', '==', 'a', optionsParam)); assert.equal(true, checkIf('1', '==', 1, optionsParam)); assert.equal(false, checkIf('a', '==', 'b', optionsParam)); assert.equal(false, checkIf(10, '==', 11, optionsParam)); }); it('should return proper value for the `equal` operator', function() { assert.equal(true, checkIf('a', 'equal', 'a', optionsParam)); assert.equal(true, checkIf('1', 'equal', 1, optionsParam)); assert.equal(false, checkIf('a', 'equal', 'b', optionsParam)); assert.equal(false, checkIf(10, 'equal', 11, optionsParam)); }); it('should return proper value for the `===` operator', function() { assert.equal(true, checkIf('a', '===', 'a', optionsParam)); assert.equal(false, checkIf('1', '===', 1, optionsParam)); assert.equal(true, checkIf(10, '===', 10, optionsParam)); assert.equal(false, checkIf(10, '===', 11, optionsParam)); }); it('should return proper value for the `strictEqual` operator', function() { assert.equal(true, checkIf('a', 'strictEqual', 'a', optionsParam)); assert.equal(false, checkIf('1', 'strictEqual', 1, optionsParam)); assert.equal(true, checkIf(10, 'strictEqual', 10, optionsParam)); assert.equal(false, checkIf(10, 'strictEqual', 11, optionsParam)); }); it('should return proper value for the `!=` operator', function() { assert.equal(false, checkIf('a', '!=', 'a', optionsParam)); assert.equal(false, checkIf('1', '!=', 1, optionsParam)); assert.equal(true, checkIf('a', '!=', 'b', optionsParam)); assert.equal(true, checkIf(10, '!=', 11, optionsParam)); }); it('should return proper value for the `different` operator', function() { assert.equal(false, checkIf('a', 'different', 'a', optionsParam)); assert.equal(false, checkIf('1', 'different', 1, optionsParam)); assert.equal(true, checkIf('a', 'different', 'b', optionsParam)); assert.equal(true, checkIf(10, 'different', 11, optionsParam)); }); it('should return proper value for the `!==` operator', function() { assert.equal(false, checkIf('a', '!==', 'a', optionsParam)); assert.equal(true, checkIf('1', '!==', 1, optionsParam)); assert.equal(false, checkIf(10, '!==', 10, optionsParam)); assert.equal(true, checkIf(10, '!==', 11, optionsParam)); }); it('should return proper value for the `strictDifferent` operator', function() { assert.equal(false, checkIf('a', 'strictDifferent', 'a', optionsParam)); assert.equal(true, checkIf('1', 'strictDifferent', 1, optionsParam)); assert.equal(false, checkIf(10, 'strictDifferent', 10, optionsParam)); assert.equal(true, checkIf(10, 'strictDifferent', 11, optionsParam)); }); it('should return proper values for the `&&` operator', function() { assert.equal(true, checkIf(true, '&&', true, optionsParam)); assert.equal(false, checkIf(false, '&&', true, optionsParam)); assert.equal(false, checkIf(true, '&&', false, optionsParam)); assert.equal(false, checkIf(false, '&&', false, optionsParam)); }); it('should return proper values for the `and` operator', function() { assert.equal(true, checkIf(true, 'and', true, optionsParam)); assert.equal(false, checkIf(false, 'and', true, optionsParam)); assert.equal(false, checkIf(true, 'and', false, optionsParam)); assert.equal(false, checkIf(false, 'and', false, optionsParam)); }); it('should return proper values for the `||` operator', function() { assert.equal(true, checkIf(true, '||', true, optionsParam)); assert.equal(true, checkIf(false, '||', true, optionsParam)); assert.equal(true, checkIf(true, '||', false, optionsParam)); assert.equal(false, checkIf(false, '||', false, optionsParam)); }); it('should return proper values for the `or` operator', function() { assert.equal(true, checkIf(true, 'or', true, optionsParam)); assert.equal(true, checkIf(false, 'or', true, optionsParam)); assert.equal(true, checkIf(true, 'or', false, optionsParam)); assert.equal(false, checkIf(false, 'or', false, optionsParam)); }); it('should return proper values for the `<` operator', function() { assert.equal(true, checkIf(10, '<', 11, optionsParam)); assert.equal(false, checkIf(10, '<', -11, optionsParam)); }); it('should return proper values for the `lesser` operator', function() { assert.equal(true, checkIf(10, 'lesser', 11, optionsParam)); assert.equal(false, checkIf(10, 'lesser', -11, optionsParam)); }); it('should return proper values for the `>` operator', function() { assert.equal(false, checkIf(10, '>', 11, optionsParam)); assert.equal(true, checkIf(10, '>', -11, optionsParam)); }); it('should return proper values for the `greater` operator', function() { assert.equal(false, checkIf(10, 'greater', 11, optionsParam)); assert.equal(true, checkIf(10, 'greater', -11, optionsParam)); }); it('should return proper values for the `<=` operator', function() { assert.equal(true, checkIf(10, '<=', 10, optionsParam)); assert.equal(true, checkIf(10, '<=', 11, optionsParam)); assert.equal(false, checkIf(10, '<=', -11, optionsParam)); }); it('should return proper values for the `lesserEqual` operator', function() { assert.equal(true, checkIf(10, 'lesserEqual', 10, optionsParam)); assert.equal(true, checkIf(10, 'lesserEqual', 11, optionsParam)); assert.equal(false, checkIf(10, 'lesserEqual', -11, optionsParam)); }); it('should return proper values for the `>=` operator', function() { assert.equal(false, checkIf(10, '>=', 11, optionsParam)); assert.equal(true, checkIf(10, '>=', -11, optionsParam)); assert.equal(true, checkIf(10, '>=', 10, optionsParam)); }); it('should return proper values for the `greaterEqual` operator', function() { assert.equal(false, checkIf(10, 'greaterEqual', 11, optionsParam)); assert.equal(true, checkIf(10, 'greaterEqual', -11, optionsParam)); assert.equal(true, checkIf(10, 'greaterEqual', 10, optionsParam)); }); it('should return proper values for the `contains` operator', function() { assert.equal(false, checkIf('10', 'contains', 11, optionsParam)); assert.equal(false, checkIf('10,11', 'contains', 12, optionsParam)); assert.equal(true, checkIf('10,11', 'contains', 11, optionsParam)); assert.equal(true, checkIf('10,11,12', 'contains', 12, optionsParam)); assert.equal(false, checkIf('10', 'contains', '11', optionsParam)); assert.equal(false, checkIf('10,11', 'contains', '12', optionsParam)); assert.equal(true, checkIf('10,11', 'contains', '11', optionsParam)); assert.equal(true, checkIf('10,11,12', 'contains', '12', optionsParam)); }); it('should return proper values for the `notContains` operator', function() { assert.equal(true, checkIf('10', 'notContains', 11, optionsParam)); assert.equal(true, checkIf('10,11', 'notContains', 12, optionsParam)); assert.equal(false, checkIf('10,11', 'notContains', 11, optionsParam)); assert.equal(false, checkIf('10,11,12', 'notContains', 12, optionsParam)); assert.equal(true, checkIf('10', 'notContains', '11', optionsParam)); assert.equal(true, checkIf('10,11', 'notContains', '12', optionsParam)); assert.equal(false, checkIf('10,11', 'notContains', '11', optionsParam)); assert.equal(false, checkIf('10,11,12', 'notContains', '12', optionsParam)); }); }); ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/specs/feed-link.spec.js ================================================ const assert = require('assert'); const Handlebars = require('handlebars'); const feedLink = require('../feed-link.js').__feedLink; describe('Handlebars - feedLink helper', function() { describe('#feedLink - both RSS and JSON', function() { let rendererInstance = { siteConfig: { domain: 'https://example.com', advanced: { feed: { enableRss: 1, enableJson: 1 } } } }; it('should return proper URL to feed.xml and feed.json files', function() { assert.equal('' + "\n" + '' + "\n", feedLink.call(rendererInstance).string); }); }); describe('#feedLink - only RSS', function() { let rendererInstance = { siteConfig: { domain: 'https://example.com', advanced: { feed: { enableRss: 1, enableJson: 0 } } } }; it('should return proper URL to feed.xml file', function() { assert.equal('' + "\n", feedLink.call(rendererInstance).string); }); }); describe('#feedLink - only JSON', function() { let rendererInstance = { siteConfig: { domain: 'https://example.com', advanced: { feed: { enableRss: 0, enableJson: 1 } } } }; it('should return proper URL to feed.json file', function() { assert.equal('' + "\n", feedLink.call(rendererInstance).string); }); }); describe('#feedLink - none', function() { let rendererInstance = { siteConfig: { domain: 'https://example.com', advanced: { feed: { enableRss: 0, enableJson: 0 } } } }; it('should return empty string', function() { assert.equal('', feedLink.call(rendererInstance).string); }); }); }); ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/specs/font.spec.js ================================================ const assert = require('assert'); const Handlebars = require('handlebars'); const font = require('../font.js'); describe('Handlebars - font helper', function() { describe('#font', function() { it('should return proper URL to Google Fonts API', function() { assert.equal('https://fonts.googleapis.com/css?family=', font.__font('')); assert.equal('https://fonts.googleapis.com/css?family=Open+Sans', font.__font('Open+Sans')); }); }); }); ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/specs/is-empty.spec.js ================================================ const assert = require('assert'); const isEmpty = require('../is-empty.js'); describe('Handlebars - isEmpty block helper', function() { // Below object emulates Handlebars behaviour let optionsParam = { fn: () => true, inverse: () => false }; it('should return true for empty array', function() { assert.equal(true, isEmpty([], optionsParam)); }); it('should return false for non-empty array', function() { assert.equal(false, isEmpty([1,2,3], optionsParam)); }); it('should return true for empty object', function() { assert.equal(true, isEmpty({}, optionsParam)); }); it('should return false for non-empty object', function() { assert.equal(false, isEmpty({a:1, b:2}, optionsParam)); }); }); ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/specs/is-not-empty.spec.js ================================================ const assert = require('assert'); const isNotEmpty = require('../is-not-empty.js'); describe('Handlebars - isNotEmpty block helper', function() { // Below object emulates Handlebars behaviour let optionsParam = { fn: () => true, inverse: () => false }; it('should return false for empty array', function() { assert.equal(false, isNotEmpty([], optionsParam)); }); it('should return true for non-empty array', function() { assert.equal(true, isNotEmpty([1,2,3], optionsParam)); }); it('should return false for empty object', function() { assert.equal(false, isNotEmpty({}, optionsParam)); }); it('should return true for non-empty object', function() { assert.equal(true, isNotEmpty({a:1, b:2}, optionsParam)); }); }); ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/specs/jsonify.spec.js ================================================ const assert = require('assert'); const Handlebars = require('handlebars'); const jsonify = require('../jsonify.js'); describe('Handlebars - jsonify helper', function() { describe('#jsonify', function() { it('should return proper value if string is a value', function() { assert.equal('"string"', jsonify('string')); }); it('should return proper value if array is a value', function() { assert.equal('[1,2,3]', jsonify([1,2,3])); }); it('should return proper value if object is a value', function() { assert.equal('{"a":1}', jsonify({a: 1})); }); }); }); ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/specs/translate.spec.js ================================================ const assert = require('assert'); const Handlebars = require('handlebars'); const translate = require('../translate.js'); describe('Handlebars - translate helper', function() { describe('#resolveObject', function() { it('should return undefined when there is no arguments', function() { assert.equal(undefined, translate.__resolveObject()); }); it('should return undefined when one of the arguments is empty', function() { assert.equal(undefined, translate.__resolveObject(undefined, false)); assert.equal(undefined, translate.__resolveObject(false)); }); it('should return undefined when the path doesn\'t exist in the passed object', function() { assert.equal(undefined, translate.__resolveObject({}, 'a')); }); it('should return proper value when the path exist in the passed object', function() { assert.equal(1, translate.__resolveObject({'a': 1}, 'a')); }); it('should return proper value when the path exist in the passed object (nested)', function() { assert.equal(1, translate.__resolveObject({'a': {'b': {'c': 1}}}, 'a.b.c')); }); }); describe('#translate', function() { it('should return `[MISSING TRANSLATION]` when there is no translations', function() { let translator = translate.__translate.bind({ translations: { user: false, theme: false } }); assert.equal('[MISSING TRANSLATION]', translator()); }); it('should return proper value when there is translations', function() { let translator = translate.__translate.bind({ translations: { user: { 'key': 'Hello' }, theme: false } }); assert.equal('Hello', translator('key')); }); it('should return proper value when there is no translations in the user\'s override', function() { let translator = translate.__translate.bind({ translations: { user: {}, theme: { 'key': 'Hello' } } }); assert.equal('Hello', translator('key')); }); it('should return user value when there is translations in the theme\'s language file', function() { let translator = translate.__translate.bind({ translations: { user: { 'key': 'Hello' }, theme: { 'key': 'Hello2' } } }); assert.equal('Hello', translator('key')); }); it('should return user value when there is nested translations object', function() { let translator = translate.__translate.bind({ translations: { user: { params: { 'key': 'Hello' } }, theme: { 'key': 'Hello2' } } }); assert.equal('Hello', translator('params.key')); }); it('should return [WRONG TRANSLATION ARGUMENTS NUMBER] when there is too few arguments', function() { let translator = translate.__translate.bind({ translations: { user: { 'key': 'Hello %s' }, theme: { 'key': 'Hello2' } } }); assert.equal('[WRONG TRANSLATION ARGUMENTS NUMBER]', translator('key', {})); }); it('should return [WRONG TRANSLATION ARGUMENTS NUMBER] when there is too much arguments', function() { let translator = translate.__translate.bind({ translations: { user: { 'key': 'Hello %s' }, theme: { 'key': 'Hello2' } } }); assert.equal('[WRONG TRANSLATION ARGUMENTS NUMBER]', translator('key', 'John', 'Doe', {})); }); it('should return properly replaced text when there is text with one replacement', function() { let translator = translate.__translate.bind({ translations: { user: { 'key': 'Hello %s' }, theme: { 'key': 'Hello2' } } }); assert.equal('Hello John', translator('key', 'John', {})); }); it('should return properly replaced text when there is text with few replacements', function() { let translator = translate.__translate.bind({ translations: { user: { 'key': 'Hello %s %s' }, theme: { 'key': 'Hello2' } } }); assert.equal('Hello John Doe', translator('key', 'John', 'Doe', {})); }); it('should return properly replaced text when the argument is a number', function() { let translator = translate.__translate.bind({ translations: { user: { 'key': 'Current date is %s %s' }, theme: { 'key': 'Hello2' } } }); assert.equal('Current date is 12 2017', translator('key', 12, 2017, {})); }); it('should return [NO NUMBER FOR THE PLURAL PHRASE] when there is no argument', function() { let translator = translate.__translate.bind({ translations: { user: { plural: { 0: 'Zero', 1: 'One', 2: 'Two', 'default': 'More' } }, theme: false } }); assert.equal('[NO NUMBER FOR THE PLURAL PHRASE]', translator('plural', {})); }); it('should return [THERE IS NO DEFINITION FOR THE DEFAULT VALUE IN THE PLURAL PHRASE] when there is no definition for the default plural value', function() { let translator = translate.__translate.bind({ translations: { user: { plural: { 0: 'Zero', 1: 'One', 2: 'Two' } }, theme: false } }); assert.equal('[THERE IS NO DEFINITION FOR THE DEFAULT VALUE IN THE PLURAL PHRASE]', translator('plural', 101, {})); }); it('should return proper translations for a different numbers in plural case', function() { let translator = translate.__translate.bind({ translations: { user: { plural: { 0: 'Zero', 1: 'One', 2: 'Two', 'default': 'More' } }, theme: false } }); assert.equal('Zero', translator('plural', 0, {})); assert.equal('One', translator('plural', 1, {})); assert.equal('Two', translator('plural', 2, {})); assert.equal('More', translator('plural', 3, {})); assert.equal('More', translator('plural', 10, {})); assert.equal('More', translator('plural', 2048, {})); }); }); }); ================================================ FILE: app/back-end/modules/render-html/handlebars/helpers/translate.js ================================================ const Handlebars = require('handlebars'); /** * Helper functin used for creating translatable phrases * * {{ translate 'key.subkey' [string1 string2 ...] }} * * @returns {string} - translation for a given strings */ function translate(key) { let translation = '[MISSING TRANSLATION]'; /* * this is pointing to the rendererInstance */ if(this.translations.user) { let result = resolveObject(this.translations.user, key); if(result) { translation = result; } else if(this.translations.theme) { let result = resolveObject(this.translations.theme, key); if (result) { translation = result; } } } else if(this.translations.theme) { let result = resolveObject(this.translations.theme, key); if(result) { translation = result; } } if(typeof translation === 'object') { // Parse complex translation (plurals) let numberToUse = Array.prototype.slice.call(arguments).slice(1); numberToUse.pop(); if(numberToUse.length != 1 || isNaN(numberToUse[0])) { translation = '[NO NUMBER FOR THE PLURAL PHRASE]'; } else { let number = parseInt(numberToUse[0], 10); if(translation[number]) { translation = translation[number]; } else { if(translation.default) { translation = translation.default; } else { translation = '[THERE IS NO DEFINITION FOR THE DEFAULT VALUE IN THE PLURAL PHRASE]'; } } } } else if(translation.indexOf('%s') > -1) { // Parse arguments and merge into translation let phrasesToReplace = Array.prototype.slice.call(arguments).slice(1); phrasesToReplace.pop(); if(translation.match(/%s/g).length !== phrasesToReplace.length) { translation = '[WRONG TRANSLATION ARGUMENTS NUMBER]'; } else { let textToTransform = translation; for(let i = 0; i < phrasesToReplace.length; i++) { textToTransform = textToTransform.replace('%s', phrasesToReplace[i]); } translation = textToTransform; } } return new Handlebars.SafeString(translation); } /** * Helper function to return a value inside the object for a given object path * * @param {object} obj - object to traverse * @param {string} path - path in the object structure * * @returns {mixed} - undefined if specific value does not exist inside the object * or the value under specific object path */ function resolveObject(obj, path) { if(!obj || !path) { return undefined; } return path.split('.').reduce(function(previous, current) { return previous ? previous[current] : undefined }, obj); } /** * Helper for creating translatable phrases * * {{ translate 'key.subkey' [string1 string2 ...] }} */ function translateHelper(rendererInstance, Handlebars) { Handlebars.registerHelper('translate', translate.bind(rendererInstance)); } module.exports = { translateHelper: translateHelper, __resolveObject: resolveObject, __translate: translate }; ================================================ FILE: app/back-end/modules/render-html/helpers/content.js ================================================ /* * Class used to help with operations on * the URLs and slugs */ const slug = require('./../../../helpers/slug'); const path = require('path'); const MarkdownToHtml = require('./../text-renderers/markdown'); const BlocksToHtml = require('./../text-renderers/blockeditor'); const normalizePath = require('normalize-path'); const URLHelper = require('./url'); const UtilsHelper = require('./../../../helpers/utils'); /** * Class used to prepare content in data items */ class ContentHelper { /** * Prepares post content * * @param postID * @param originalText * @param siteDomain * @param themeConfig * @param renderer * @returns {string} */ static prepareContent(postID, originalText, siteDomain, themeConfig, renderer, editor = 'tinymce') { let domain = normalizePath(siteDomain); domain = URLHelper.fixProtocols(domain); // Get media URL let domainMediaPath = domain + '/media/posts/' + postID + '/'; // Detect forced WebP images let useWebp = false; if (renderer.siteConfig?.advanced?.forceWebp) { useWebp = true; } // Replace domain name constant with real URL to media directory let preparedText = originalText.split('#DOMAIN_NAME#').join(domainMediaPath); preparedText = ContentHelper.parseText(preparedText, editor); preparedText = ContentHelper.setWebpCompatibility(useWebp, preparedText); // Remove content for AMP or non-AMP depending from ampMode value preparedText = preparedText.replace(/[\s\S]*?<\/publii-amp>/gmi, ''); preparedText = preparedText.replace(//gmi, ''); preparedText = preparedText.replace(/<\/publii-non-amp>/gmi, ''); // Remove TOC plugin ID attributes when TOC does not exist if (preparedText.indexOf('class="post__toc') === -1) { preparedText = preparedText.replace(/\sid="mcetoc_[a-z0-9]*?"/gmi, ''); } // Reduce download="download" to download preparedText = preparedText.replace(/download="download"/gmi, 'download'); // Remove the last empty paragraph preparedText = preparedText.replace(/

     <\/p>\s?$/gmi, ''); // Find all images and add srcset and sizes attributes if (renderer.siteConfig.advanced.responsiveImages) { preparedText = preparedText.replace(//gmi, function(matches, url) { if (matches.indexOf('data-responsive="false"') > -1) { return matches; } return ContentHelper._addResponsiveAttributes(matches, url, themeConfig, useWebp, domain); }); } // Add loading="lazy" attributes to img, video, audio, iframe tags if (renderer.siteConfig.advanced.mediaLazyLoad) { preparedText = preparedText.replace(/].*?\sloading="[^>].*?>)/gmi, '].*?\sloading="[^>].*?>)/gmi, '].*?\sloading="[^>].*?>)/gmi, '].*?\sloading="[^>].*?>)/gmi, ' preparedText = preparedText.replace(/(\s*?)?]*?(class=".*?").*?>(\s*?<\/p>)?/gmi, function(matches, p1, classes) { return '

    ' + matches.replace('

    ', '').replace(//, '').replace(classes, '') + '
    '; }); // Fix some specific syntax cases for double figure elements preparedText = preparedText.replace(/
    [\s]*?
    ([\s\S]*?)<\/figure>[\s]*?<\/figure>/gmi, '
    $1
    '); preparedText = preparedText.replace(/
    [\s]*?
    ([\s\S]*?)<\/figure>[\s]*?
    ([\s\S]*?)<\/figcaption>[\s]*?<\/figure>/gmi, '
    $1
    $2
    '); } // Remove contenteditable attributes preparedText = preparedText.replace(/contenteditable=".*?"/gi, ''); if (editor === 'tinymce') { // Wrap galleries with classes into div with gallery-wrapper CSS class preparedText = preparedText.replace(/
    ?/gmi, function(matches, classes) { return ''; }); } // Remove double slashes from the gallery URLs (if they appears) preparedText = preparedText.replace(/\/\/gallery\/$/gmi, '/gallery/'); // Remove paragraphs around '); // Wrap iframes into
    preparedText = preparedText.replace(/(?[\s\S]*?)([\s\S]*?<\/iframe>)/gmi, function(matches) { if (matches.indexOf('data-responsive="false"') > -1) { return matches; } return '
    ' + matches + '
    '; }); // Remove CDATA sections inside scripts added by TinyMCE preparedText = preparedText.replace(/\\/\/ \<\!\[CDATA\[/g, ''); // Add embed consents if ( themeConfig.supportedFeatures && themeConfig.supportedFeatures.embedConsents && renderer.siteConfig.advanced.gdpr.enabled && renderer.siteConfig.advanced.gdpr.allowAdvancedConfiguration && renderer.siteConfig.advanced.gdpr.embedConsents && renderer.siteConfig.advanced.gdpr.embedConsents.length ) { preparedText = ContentHelper.addEmbedConsents(preparedText, renderer.siteConfig.advanced.gdpr.embedConsents); } // Add dnt=1 for Vimeo links if (renderer.siteConfig.advanced.gdpr.vimeoNoTrack) { preparedText = preparedText.replace(/src="(http[s]?\:\/\/player\.vimeo\.com\/video\/.*?)"/gmi, function (url, matches) { if (matches.indexOf('dnt=') > -1) { return 'src="' + matches + '"'; } if (matches.indexOf('?') > -1) { return 'src="' + matches + '&dnt=1"'; } return 'src="' + matches + '?dnt=1"'; }); preparedText = preparedText.replace(/src='(http[s]?\:\/\/player\.vimeo\.com\/video\/.*?)'/gmi, function (url, matches) { if (matches.indexOf('dnt=') > -1) { return 'src=\'' + matches + '\''; } if (matches.indexOf('?') > -1) { return 'src=\'' + matches + '&dnt=1\''; } return 'src=\'' + matches + '?dnt=1\''; }); } // Add youtube-nocookie.com domain for YouTube videos if (renderer.siteConfig.advanced.gdpr.ytNoCookies) { preparedText = preparedText.replace(/src="http[s]?\:\/\/www\.youtube\.com\/embed\//gmi, 'src="https://www.youtube-nocookie.com/embed/'); preparedText = preparedText.replace(/src='http[s]?\:\/\/www\.youtube\.com\/embed\//gmi, 'src=\'https://www.youtube-nocookie.com/embed/'); } return preparedText; } /** * Parse text using a editor-specific parser * * @param {*} inputText * @param {*} editor */ static parseText (inputText, editor = 'tinymce') { if (editor === 'tinymce') { return inputText; } if (editor === 'markdown') { inputText = ContentHelper.prepareMarkdown(inputText); return MarkdownToHtml.parse(inputText); } if (editor === 'blockeditor') { return BlocksToHtml.parse(inputText); } } /** * Prepares markdown code to display * @param input */ static prepareMarkdown (input) { input = input.replace(/\-\-\-READMORE\-\-\-/gmi, '
    '); return input; } /** * Prepares post excerpt * * @param length * @param text * @returns {*} */ static prepareExcerpt(length, text) { // Detect readmore let readmoreMatches = text.match(/\/gmi); if(readmoreMatches && readmoreMatches.length) { text = text.split(/\/gmi); text = text[0]; return text; } length = parseInt(length, 10); text = text.replace(/\\/\/ \<\!\[CDATA\[/g, ''); text = text.replace(/\
    [\s\S]*?\<\/div\>/gmi, ''); // Remove ToC text = text.replace(/
    `; return iframe; }); } return text; } /** * Replaces non-WebP images to WebP or WebP images to non-WebP images in gallery thumbnails if necessary * @param {boolean} forceWebp - state of force WebP option * @param {string} text - text to modify * @returns {string} - modified text */ static setWebpCompatibility (forceWebp, text) { text = text.replace(/\`; } return html; }; module.exports = render; ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-list/block.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-list/config-form.json ================================================ [ { "name": "cssClasses", "type": "text", "label": "editor.blocks.cssClassesLabel", "tooltip": "editor.blocks.cssClassesTooltip", "defaultValue": "" }, { "name": "id", "type": "text", "label": "editor.blocks.idLabel", "tooltip": "editor.blocks.list.idTooltip", "defaultValue": "" } ] ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-list/conversions.js ================================================ const availableConversions = [ { 'icon': 'paragraph', 'name': 'editor.conversions.toParagraph', 'type': 'publii-paragraph', 'convert': function (config, content, editorInstance) { // eslint-disable-next-line let newContent = content.replace(//gmi, '') // eslint-disable-next-line .replace(//gmi, '') // eslint-disable-next-line .replace(/<\/li>
  • /gmi, "
    ") // eslint-disable-next-line .replace(/\n/gmi, '
    ') // eslint-disable-next-line .replace(//gmi, '') // eslint-disable-next-line .replace(/<\/li.*?>/gmi, ''); let newConfig = { textAlign: 'left', advanced: { style: '', cssClasses: config.advanced.cssClasses, id: config.advanced.id } }; return { content: newContent, config: newConfig }; } }, { 'icon': 'html', 'name': 'editor.conversions.toHTML', 'type': 'publii-html', 'convert': function (config, content, editorInstance, rawBlock) { let newContent = rawBlock.outerHTML // eslint-disable-next-line .replace(//gmi, '
      ') // eslint-disable-next-line .replace(//gmi, '
        ') // eslint-disable-next-line .replace(/<\/li>
      1. /gmi, "
      2. \n
      3. ") // eslint-disable-next-line .replace(/ul>
      4. /gmi, "ul>\n
      5. ") // eslint-disable-next-line .replace(/ol>
      6. /gmi, "ol>\n
      7. ") // eslint-disable-next-line .replace(/<\/li><\/ul>/gmi, "
      8. \n
    "); let newConfig = { advanced: { cssClasses: config.advanced.cssClasses, id: config.advanced.id } }; return { content: newContent, config: newConfig }; } } ]; module.exports = availableConversions; ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-list/render.js ================================================ function render (blockData) { let id = blockData.config.advanced.id ? ' id="' + blockData.config.advanced.id + '"' : ''; let cssClasses = blockData.config.advanced.cssClasses ? ' class="' + blockData.config.advanced.cssClasses + '"' : ''; let listType = blockData.config.listType; let html = ` <${listType}${id}${cssClasses}> ${blockData.content} `; return html; }; module.exports = render; ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-paragraph/block.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-paragraph/config-form.json ================================================ [ { "name": "cssClasses", "type": "text", "label": "editor.blocks.cssClassesLabel", "tooltip": "editor.blocks.cssClassesTooltip", "defaultValue": "" }, { "name": "style", "type": "select", "label": "editor.blocks.paragraph.styleLabel", "tooltip": "editor.blocks.paragraph.styleTooltip", "values": [ { "value": "msg msg--info", "label": "editor.blocks.paragraph.styles.info" }, { "value": "msg msg--highlight", "label": "editor.blocks.paragraph.styles.highlight" }, { "value": "msg msg--success", "label": "editor.blocks.paragraph.styles.success" }, { "value": "msg msg--warning", "label": "editor.blocks.paragraph.styles.warning" } ], "defaultValue": "" }, { "name": "id", "type": "text", "label": "editor.blocks.idLabel", "tooltip": "editor.blocks.paragraph.idTooltip", "defaultValue": "" } ] ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-paragraph/conversions.js ================================================ const availableConversions = [ { 'icon': 'headings', 'name': 'editor.conversions.toHeader', 'type': 'publii-header', 'convert': function (config, content, editorInstance) { // eslint-disable-next-line let newContent = editorInstance.extensions.conversionHelpers.stripTags(content.replace(/
    /gmi, "\n")).replace(/\n/gmi, '
    '); let newConfig = { headingLevel: 2, textAlign: config.textAlign, link: { url: '', noFollow: false, targetBlank: false, sponsored: false, ugc: false }, advanced: { cssClasses: config.advanced.cssClasses, customId: false, id: config.advanced.id } }; return { content: newContent, config: newConfig }; } }, { 'icon': 'quote', 'name': 'editor.conversions.toQuote', 'type': 'publii-quote', 'convert': function (config, content, editorInstance) { let newConfig = { advanced: { cssClasses: config.advanced.cssClasses, id: config.advanced.id } }; return { content: { text: content, author: '' }, config: newConfig }; } }, { 'icon': 'unordered-list', 'name': 'editor.conversions.toList', 'type': 'publii-list', 'convert': function (config, content, editorInstance) { let newContent = '
  • ' + content.split('
    ').join('
  • ') + '
  • '; let newConfig = { listType: 'ul', advanced: { cssClasses: config.advanced.cssClasses, id: config.advanced.id } }; return { content: newContent, config: newConfig }; } }, { 'icon': 'code', 'name': 'editor.conversions.toCode', 'type': 'publii-code', 'convert': function (config, content, editorInstance) { // eslint-disable-next-line let newContent = editorInstance.extensions.conversionHelpers.stripTags(content.replace(/
    /gmi, "\n")); let newConfig = { language: 'html', advanced: { cssClasses: config.advanced.cssClasses, id: config.advanced.id } }; return { content: newContent, config: newConfig }; } }, { 'icon': 'html', 'name': 'editor.conversions.toHTML', 'type': 'publii-html', 'convert': function (config, content, editorInstance, rawBlock) { let newContent = rawBlock.outerHTML.replace(//gmi, '

    '); let newConfig = { advanced: { cssClasses: config.advanced.cssClasses, id: config.advanced.id } }; return { content: newContent, config: newConfig }; } } ]; module.exports = availableConversions; ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-paragraph/render.js ================================================ function render (blockData) { let id = blockData.config.advanced.id ? ' id="' + blockData.config.advanced.id + '"' : ''; let cssClasses = [blockData.config.advanced.cssClasses, blockData.config.advanced.style, 'align-' + blockData.config.textAlign].filter(item => item && item.trim() !== '' && item !== 'align-left'); cssClasses = cssClasses.length ? ' class="' + cssClasses.join(' ') + '"' : ''; let html = ` ${blockData.content}

    `; return html; }; module.exports = render; ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-quote/block.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-quote/config-form.json ================================================ [ { "name": "cssClasses", "type": "text", "label": "editor.blocks.cssClassesLabel", "tooltip": "editor.blocks.cssClassesTooltip", "defaultValue": "" }, { "name": "id", "type": "text", "label": "editor.blocks.idLabel", "tooltip": "editor.blocks.quote.idTooltip", "defaultValue": "" } ] ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-quote/conversions.js ================================================ const availableConversions = [ { 'icon': 'paragraph', 'name': 'editor.conversions.toParagraph', 'type': 'publii-paragraph', 'convert': function (config, content, editorInstance) { let newConfig = { textAlign: 'left', advanced: { style: '', cssClasses: config.advanced.cssClasses, id: config.advanced.id } }; return { content: content.text, config: newConfig }; } }, { 'icon': 'html', 'name': 'editor.conversions.toHTML', 'type': 'publii-html', 'convert': function (config, content, editorInstance, rawBlock) { let newContent = content; let newConfig = { advanced: { cssClasses: config.advanced.cssClasses, id: config.advanced.id } }; return { content: newContent, config: newConfig }; } } ]; module.exports = availableConversions; ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-quote/render.js ================================================ function render (blockData) { let id = blockData.config.advanced.id ? ' id="' + blockData.config.advanced.id + '"' : ''; let cssClasses = ['blockquote', blockData.config.advanced.cssClasses].filter(item => item && item.trim() !== ''); cssClasses = cssClasses.length ? ' class="' + cssClasses.join(' ') + '"' : ''; let html = ''; if (blockData.content.author.trim() !== '') { html = `
    ${blockData.content.text}
    ${blockData.content.author}
    `; } else { html = ` ${blockData.content.text} `; } return html; }; module.exports = render; ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-readmore/block.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-readmore/config-form.json ================================================ [] ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-readmore/render.js ================================================ function render () { return `
    `; }; module.exports = render; ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-separator/block.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-separator/config-form.json ================================================ [ { "name": "cssClasses", "type": "text", "label": "editor.blocks.cssClassesLabel", "tooltip": "editor.blocks.cssClassesTooltip", "defaultValue": "" }, { "name": "id", "type": "text", "label": "editor.blocks.idLabel", "tooltip": "editor.blocks.separator.idTooltip", "defaultValue": "" } ] ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-separator/conversions.js ================================================ const availableConversions = [ { 'icon': 'html', 'name': 'editor.conversions.toHTML', 'type': 'publii-html', 'convert': function (config, content, editorInstance, rawBlock) { let newContent = rawBlock.innerHTML; let newConfig = { advanced: { cssClasses: config.advanced.cssClasses, id: config.advanced.id } }; return { content: newContent, config: newConfig }; } } ]; module.exports = availableConversions; ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-separator/render.js ================================================ function render (blockData) { let id = blockData.config.advanced.id ? ' id="' + blockData.config.advanced.id + '"' : ''; let cssClasses = ['separator', blockData.config.advanced.cssClasses, 'separator--' + blockData.config.type].filter(item => item && item.trim() !== ''); cssClasses = cssClasses.length ? ' class="' + cssClasses.join(' ') + '"' : ''; let html = ``; return html; }; module.exports = render; ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-toc/block.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-toc/config-form.json ================================================ [ { "name": "cssClasses", "type": "text", "label": "editor.blocks.cssClassesLabel", "tooltip": "editor.blocks.cssClassesTooltip", "defaultValue": "" }, { "name": "id", "type": "text", "label": "editor.blocks.idLabel", "tooltip": "editor.blocks.toc.idTooltip", "defaultValue": "" } ] ================================================ FILE: app/src/components/block-editor/components/default-blocks/publii-toc/render.js ================================================ function render (blockData) { let id = blockData.config.advanced.id ? ' id="' + blockData.config.advanced.id + '"' : ''; let cssClasses = ['post__toc', blockData.config.advanced.cssClasses].filter(item => item && item.trim() !== ''); cssClasses = cssClasses.length ? ' class="' + cssClasses.join(' ') + '"' : ''; let tocHeading = ''; if (blockData.content.title.trim() !== '') { tocHeading = `

    ${blockData.content.title}

    `; } let html = ` ${tocHeading}
      ${blockData.content.toc.replace(/\
        /gmi, '
    ')}
    `; return html; }; module.exports = render; ================================================ FILE: app/src/components/block-editor/components/elements/EditorIcon.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/elements/Switcher.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/extensions/ConversionHelpers.js ================================================ export default class ConversionHelpers { stripTags (input, saveLineBreaks = true) { let div = document.createElement('div'); div.innerHTML = input.replace(/
    /gmi, '[[[BR]]]').replace(/
    /gmi, '[[[BR]]]').replace(//gmi, '[[[BR]]]'); // eslint-disable-next-line let output = div.innerText.replace(/\[\[\[BR\]\]\]/gmi, "\n"); return output; } } ================================================ FILE: app/src/components/block-editor/components/extensions/ShortcutManager.js ================================================ export default class ShortcutManager { constructor () { this.shortcuts = {}; this.initDefaultShortcuts(); this.initMarkdownDefaultShortcuts(); } initDefaultShortcuts () { this.shortcuts['/separator'] = 'publii-separator'; this.shortcuts['/hr'] = 'publii-separator'; this.shortcuts['/header'] = 'publii-header'; this.shortcuts['/h1'] = 'publii-header-1'; this.shortcuts['/h2'] = 'publii-header-2'; this.shortcuts['/h3'] = 'publii-header-3'; this.shortcuts['/h4'] = 'publii-header-4'; this.shortcuts['/h5'] = 'publii-header-5'; this.shortcuts['/h6'] = 'publii-header-6'; this.shortcuts['/list'] = 'publii-list'; this.shortcuts['/quote'] = 'publii-quote'; this.shortcuts['/blockquote'] = 'publii-quote'; this.shortcuts['/code'] = 'publii-code'; this.shortcuts['/readmore'] = 'publii-readmore'; this.shortcuts['/more'] = 'publii-readmore'; this.shortcuts['/html'] = 'publii-html'; this.shortcuts['/toc'] = 'publii-toc'; // this.shortcuts['/embed'] = 'publii-embed'; this.shortcuts['/image'] = 'publii-image'; this.shortcuts['/img'] = 'publii-image'; this.shortcuts['/gallery'] = 'publii-gallery'; } initMarkdownDefaultShortcuts () { this.shortcuts['---'] = 'publii-separator'; this.shortcuts['***'] = 'publii-readmore'; this.shortcuts['#'] = 'publii-header-1'; this.shortcuts['##'] = 'publii-header-2'; this.shortcuts['###'] = 'publii-header-3'; this.shortcuts['####'] = 'publii-header-4'; this.shortcuts['#####'] = 'publii-header-5'; this.shortcuts['######'] = 'publii-header-6'; this.shortcuts['*'] = 'publii-list'; this.shortcuts['>'] = 'publii-quote'; this.shortcuts['```'] = 'publii-code'; } checkContentForShortcuts (text) { if (text !== '' && text.length < 24 && this.shortcuts[text.trim()]) { return this.shortcuts[text.trim()]; } return 'publii-paragraph'; } add (shortcut, componentName) { if (!this.shortcuts[shortcut]) { this.shortcuts[shortcut] = componentName; } else { console.warn('The following shortcut is already defined: ' + shortcut + ' for the following block: ' + this.shortcuts[shortcut]); } } }; ================================================ FILE: app/src/components/block-editor/components/extensions/UndoManager.js ================================================ export default class UndoManager { constructor () { this.history = []; this.historyMaxLength = 500; } saveHistory (blockID, blockContent) { let historyLength = this.history.unshift({ id: blockID, content: JSON.parse(JSON.stringify(blockContent)) }); if (historyLength > this.historyMaxLength) { this.history = this.history.slice(0, this.historyMaxLength - 1); } } undoHistory (blockID) { for (let i = 0; i < this.history.length; i++) { if (this.history[i].id === blockID) { let content = JSON.parse(JSON.stringify(this.history[i].content)); this.history.splice(i, 1); return content; } } } redoHistory (blockID) { } }; ================================================ FILE: app/src/components/block-editor/components/helpers/ContentEditableImprovements.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/helpers/InlineMenuUI.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/helpers/TopMenuUI.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/mixins/AdvancedConfig.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/mixins/HasPreview.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/mixins/InlineMenu.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/mixins/LinkConfig.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/mixins/LinkHelpers.vue ================================================ ================================================ FILE: app/src/components/block-editor/components/utils/SelectedText.js ================================================ import Vue from 'vue'; export default class SelectedText { constructor (inlineMenuContainer, blockType) { this.blockType = blockType; this.inlineMenuContainer = inlineMenuContainer; this.containedTags = { strong: false, em: false, u: false, s: false, code: false, mark: false, a: false }; this.allowedOperations = { indent: true, outdent: true, clearFormatting: false }; this.tagsToCheck = [ 'strong', 'em', 'u', 's', 'code', 'mark', 'a' ]; } isInvalidTextSelection () { let selection = document.getSelection(); return !selection || !selection.anchorNode || !selection.focusNode; } analyzeSelectedText () { if (this.isInvalidTextSelection()) { return; } let range = document.getSelection().getRangeAt(0); let commonAncestor = range.commonAncestorContainer; let tempElement = document.createElement('div'); tempElement.appendChild(range.cloneContents()); let htmlToCheck = tempElement.innerHTML; if (commonAncestor.nodeType === 3) { commonAncestor = commonAncestor.parentNode; } for (let i = 0; i < this.tagsToCheck.length; i++) { let tag = this.tagsToCheck[i]; if (htmlToCheck.indexOf('<' + tag + ' ') > -1 || htmlToCheck.indexOf('') > -1) { Vue.set(this.containedTags, this.tagsToCheck[i], true); } else { if (commonAncestor.tagName === tag.toUpperCase() || commonAncestor.closest(tag) !== null) { Vue.set(this.containedTags, this.tagsToCheck[i], true); } else { Vue.set(this.containedTags, this.tagsToCheck[i], false); } } } tempElement.remove(); if (this.blockType === 'publii-list') { Vue.set(this.allowedOperations, 'indent', this.checkIfElementCanBeNested()); Vue.set(this.allowedOperations, 'outdent', this.checkIfElementCanBeFlattened()); } } removeStyle (tag) { let range = document.getSelection().getRangeAt(0); let elementToWrap = null; if (range.startContainer.nodeType === 1 && range.startContainer.tagName === tag.toUpperCase()) { elementToWrap = range.startContainer; } else if (range.startContainer.nodeType === 3 && range.startContainer.parentNode.tagName === tag.toUpperCase()) { elementToWrap = range.startContainer.parentNode; } else if (range.startContainer.nodeType === 1 && range.startContainer.querySelector(tag)) { elementToWrap = range.startContainer.querySelector(tag); } else if (range.startContainer.nodeType === 3 && range.startContainer.parentNode.querySelector(tag)) { elementToWrap = range.startContainer.parentNode.querySelector(tag); } else if (range.startContainer.nodeType === 1 && range.startContainer.closest(tag)) { elementToWrap = range.startContainer.closest(tag); } else if (range.startContainer.nodeType === 3 && range.startContainer.parentNode.closest(tag)) { elementToWrap = range.startContainer.parentNode.closest(tag); } else if (range.endContainer.nodeType === 1 && range.endContainer.tagName === tag.toUpperCase()) { elementToWrap = range.endContainer; } else if (range.endContainer.nodeType === 3 && range.endContainer.parentNode.tagName === tag.toUpperCase()) { elementToWrap = range.endContainer.parentNode; } else if (range.startContainer.nodeType === 1 && range.endContainer.querySelector(tag)) { elementToWrap = range.endContainer.querySelector(tag); } else if (range.startContainer.nodeType === 3 && range.endContainer.parentNode.querySelector(tag)) { elementToWrap = range.endContainer.parentNode.querySelector(tag); } else if (range.endContainer.nodeType === 1 && range.endContainer.closest(tag)) { elementToWrap = range.endContainer.closest(tag); } else if (range.endContainer.nodeType === 3 && range.endContainer.parentNode.closest(tag)) { elementToWrap = range.endContainer.parentNode.closest(tag); } if (elementToWrap) { range.setStartBefore(elementToWrap); range.setEndAfter(elementToWrap); setTimeout(() => { let extractedContent = range.extractContents(); let extractedContentChildren = extractedContent.children; let nodesToInsert = extractedContentChildren[0].childNodes; let firstNode = nodesToInsert[0]; let lastNode = nodesToInsert[nodesToInsert.length - 1]; for (let i = nodesToInsert.length - 1; i >= 0; i--) { range.insertNode(nodesToInsert[i]); } setTimeout(() => { range.setStartBefore(firstNode); range.setEndAfter(lastNode); }, 0); }, 0); } } checkIfElementCanBeNested () { let baseItem = document.getSelection().baseNode; let parentList; let listItem; if (document.getSelection().baseNode.nodeType === 3) { baseItem = document.getSelection().baseNode.parentNode; } parentList = baseItem.closest('ul,ol'); if (document.getSelection().baseNode.tagName === 'LI') { listItem = baseItem; } else { listItem = baseItem.closest('li'); } if (parentList.children.length <= 1 || parentList.children[0] === listItem) { return false; } return true; } checkIfElementCanBeFlattened () { let baseItem = document.getSelection().baseNode; let parentList; if (document.getSelection().baseNode.nodeType === 3) { baseItem = document.getSelection().baseNode.parentNode; } parentList = baseItem.closest('ul,ol'); if (parentList !== null && parentList !== parentList.closest('.publii-block-list')) { return true; } return false; } } ================================================ FILE: app/src/components/block-editor/components/utils/Utils.js ================================================ export default class Utils { /* * Deep merge for objects as Object.assign not merge objects properly */ static deepMerge (target, source) { if (typeof target !== 'object') { target = {}; } for (let property in source) { if (source.hasOwnProperty(property)) { let sourceProperty = source[property]; if (typeof sourceProperty === 'object' && !Array.isArray(sourceProperty) && !(sourceProperty instanceof Date)) { target[property] = Utils.deepMerge(target[property], sourceProperty); continue; } else if (sourceProperty instanceof Date) { target[property] = new Date(sourceProperty.getTime()); continue; } target[property] = sourceProperty; } } for (let a = 2, l = arguments.length; a < l; a++) { Utils.deepMerge(target, arguments[a]); } return target; } /* * Run function if it is not invoked since X ms. */ static debounce (func, wait, immediate) { var timeout; return function () { var context = this; var args = arguments; var later = function () { timeout = null; if (!immediate) { func.apply(context, args); } }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) { func.apply(context, args); } }; } } ================================================ FILE: app/src/components/block-editor/vendors/_modularscale.scss ================================================ // Defaults and variables @import 'modularscale/vars'; // Core functions @import 'modularscale/settings'; @import 'modularscale/pow'; @import 'modularscale/strip-units'; @import 'modularscale/sort'; @import 'modularscale/target'; @import 'modularscale/function'; @import 'modularscale/round-px'; // Mixins @import 'modularscale/respond'; // Syntax sugar @import 'modularscale/sugar'; ================================================ FILE: app/src/components/block-editor/vendors/modularscale/_function.scss ================================================ @function ms-function($v: 0, $base: false, $ratio: false, $thread: false, $settings: $modularscale) { // Parse settings $ms-settings: ms-settings($base,$ratio,$thread,$settings); $base: nth($ms-settings, 1); $ratio: nth($ms-settings, 2); // Render target values from settings. @if unit($ratio) != '' { $ratio: ms-target($ratio,$base) } // Fast calc if not multi stranded @if(length($base) == 1) { @return ms-pow($ratio, $v) * $base; } // Create new base array $ms-bases: nth($base,1); // Normalize base values @for $i from 2 through length($base) { // initial base value $ms-base: nth($base,$i); // If the base is bigger than the main base @if($ms-base > nth($base,1)) { // divide the value until it aligns with main base. @while($ms-base > nth($base,1)) { $ms-base: $ms-base / $ratio; } $ms-base: $ms-base * $ratio; } // If the base is smaller than the main base. @else if ($ms-base < nth($base,1)) { // pump up the value until it aligns with main base. @while $ms-base < nth($base,1) { $ms-base: $ms-base * $ratio; } } // Push into new array $ms-bases: append($ms-bases,$ms-base); } // Sort array from smallest to largest. $ms-bases: ms-sort($ms-bases); // Find step to use in calculation $vtep: floor($v / length($ms-bases)); // Find base to use in calculation $ms-base: round(($v / length($ms-bases) - $vtep) * length($ms-bases)) + 1; @return ms-pow($ratio, $vtep) * nth($ms-bases,$ms-base); } ================================================ FILE: app/src/components/block-editor/vendors/modularscale/_pow.scss ================================================ @use "sass:math"; // Sass does not have native pow() support so this needs to be added. // Compass and other libs implement this more extensively. // In order to keep this simple, use those when they are avalible. // Issue for pow() support in Sass: https://github.com/sass/sass/issues/684 @function ms-pow($b,$e) { // Return 1 if exponent is 0 @if $e == 0 { @return 1; } // If pow() exists (compass or mathsass) use that. @if function-exists('pow') { @return pow($b,$e); } // This does not support non-integer exponents, // Check and return an error if a non-integer exponent is passed. @if (floor($e) != $e) { @error 'Non-integer values are not supported in modularscale by default. Try using mathsass in your project to add non-integer scale support. https://github.com/terkel/mathsass' } // Seed the return. $ms-return: $b; // Multiply or divide by the specified number of times. @if $e > 0 { @for $i from 1 to $e { $ms-return: $ms-return * $b; } } @if $e < 0 { @for $i from $e through 0 { $ms-return: math.div($ms-return, $b); } } @return $ms-return; } ================================================ FILE: app/src/components/block-editor/vendors/modularscale/_respond.scss ================================================ // Generate calc() function // based on Mike Riethmuller's Precise control over responsive typography // http://madebymike.com.au/writing/precise-control-responsive-typography/ @function ms-fluid($val1: 1em, $val2: 1em, $break1: 0, $break2: 0) { $diff: ms-unitless($val2) - ms-unitless($val1); // v1 + (v2 - v1) * ( (100vw - b1) / b2 - b1 ) @return calc( #{$val1} + #{ms-unitless($val2) - ms-unitless($val1)} * ( ( 100vw - #{$break1}) / #{ms-unitless($break2) - ms-unitless($break1)} ) ); } // Main responsive mixin @mixin ms-respond($prop, $val, $map: $modularscale, $ms-important: false) { $base: $ms-base; $ratio: $ms-ratio; $first-write: true; $last-break: null; $important: ''; @if $ms-important == true { $important: ' !important'; } // loop through all settings with a breakpoint type value @each $v, $s in $map { @if type-of($v) == number { @if unit($v) != '' { // Write out the first value without a media query. @if $first-write { #{$prop}: unquote("#{ms-function($val, $thread: $v, $settings: $map)}#{$important}"); // Not the first write anymore, reset to false to move on. $first-write: false; $last-break: $v; } // Write intermediate breakpoints. @else { @media (min-width: $last-break) and (max-width: $v) { $val1: ms-function($val, $thread: $last-break, $settings: $map); $val2: ms-function($val, $thread: $v, $settings: $map); #{$prop}: unquote("#{ms-fluid($val1,$val2,$last-break,$v)}#{$important}"); } $last-break: $v; } } } } // Write the last breakpoint. @if $last-break { @media (min-width: $last-break) { #{$prop}: unquote("#{ms-function($val, $thread: $last-break, $settings: $map)}#{$important}"); } } } ================================================ FILE: app/src/components/block-editor/vendors/modularscale/_round-px.scss ================================================ @function ms-round-px($r) { @if unit($r) == 'px' { @return round($r); } @warn "ms-round-px is no longer used by modular scale and will be removed in the 3.1.0 release."; @return $r; } ================================================ FILE: app/src/components/block-editor/vendors/modularscale/_settings.scss ================================================ // Parse settings starting with defaults. // Settings should cascade down like you would expect in CSS. // More specific overrides previous settings. @function ms-settings($b: false, $r: false, $t: false, $m: $modularscale) { $base: $ms-base; $ratio: $ms-ratio; $thread: map-get($m, $t); // Override with user settings @if map-get($m, base) { $base: map-get($m, base); } @if map-get($m, ratio) { $ratio: map-get($m, ratio); } // Override with thread settings @if $thread { @if map-get($thread, base) { $base: map-get($thread, base); } @if map-get($thread, ratio) { $ratio: map-get($thread, ratio); } } // Override with inline settings @if $b { $base: $b; } @if $r { $ratio: $r; } @return $base $ratio; } ================================================ FILE: app/src/components/block-editor/vendors/modularscale/_sort.scss ================================================ // Basic list sorting // Would like to replace with http://sassmeister.com/gist/30e4863bd03ce0e1617c // Unfortunately libsass has a bug with passing arguments into the min() funciton. @function ms-sort($l) { // loop until the list is confirmed to be sorted $sorted: false; @while $sorted == false { // Start with the assumption that the lists are sorted. $sorted: true; // Loop through the list, checking each value with the one next to it. // Swap the values if they need to be swapped. // Not super fast but simple and modular scale doesn't lean hard on sorting. @for $i from 2 through length($l) { $n1: nth($l,$i - 1); $n2: nth($l,$i); // If the first value is greater than the 2nd, swap them. @if $n1 > $n2 { $l: set-nth($l, $i, $n1); $l: set-nth($l, $i - 1, $n2); // The list isn't sorted and needs to be looped through again. $sorted: false; } } } // Return the sorted list. @return $l; } ================================================ FILE: app/src/components/block-editor/vendors/modularscale/_strip-units.scss ================================================ // Stripping units is not a best practice // This function should not be used elsewhere // It is used here because calc() doesn't do unit logic // AND target ratios use units as a hack to get a number. @function ms-unitless($val) { @return ($val / ($val - $val + 1)); } ================================================ FILE: app/src/components/block-editor/vendors/modularscale/_sugar.scss ================================================ // To attempt to avoid conflicts with other libraries // all funcitons are namespaced with `ms-`. // However, to increase usability, a shorthand function is included here. @function ms($v: 0, $base: false, $ratio: false, $thread: false, $settings: $modularscale) { @return ms-function($v, $base, $ratio, $thread, $settings); } ================================================ FILE: app/src/components/block-editor/vendors/modularscale/_target.scss ================================================ // Convert number string to number @function ms-to-num($n) { $l: str-length($n); $r: 0; $m: str-index($n,'.'); @if $m == null { $m: $l + 1; } // Loop through digits and convert to numbers @for $i from 1 through $l { $v: str-slice($n,$i,$i); @if $v == '1' { $v: 1; } @else if $v == '2' { $v: 2; } @else if $v == '3' { $v: 3; } @else if $v == '4' { $v: 4; } @else if $v == '5' { $v: 5; } @else if $v == '6' { $v: 6; } @else if $v == '7' { $v: 7; } @else if $v == '8' { $v: 8; } @else if $v == '9' { $v: 9; } @else if $v == '0' { $v: 0; } @else { $v: null; } @if $v != null { $m: $m - 1; $r: $r + ms-pow(10,$m - 1) * $v; } @else { $l: $l - 1; } } @return $r; } // Find a ratio based on a target value @function ms-target($t,$b) { // Convert to string $t: $t + ''; // Remove base units to calulate ratio $b: ms-unitless(nth($b,1)); // Find where 'at' is in the string $at: str-index($t,'at'); // Slice the value and target out // and convert strings to numbers $v: ms-to-num(str-slice($t,0,$at - 1)); $t: ms-to-num(str-slice($t,$at + 2)); // Solve the modular scale function for the ratio. @return ms-pow(($v/$b),(1/$t)); } ================================================ FILE: app/src/components/block-editor/vendors/modularscale/_vars.scss ================================================ // Ratios $double-octave : 4 ; $pi : 3.14159265359 ; $major-twelfth : 3 ; $major-eleventh : 2.666666667 ; $major-tenth : 2.5 ; $octave : 2 ; $major-seventh : 1.875 ; $minor-seventh : 1.777777778 ; $major-sixth : 1.666666667 ; $phi : 1.618034 ; $golden : $phi ; $minor-sixth : 1.6 ; $fifth : 1.5 ; $augmented-fourth : 1.41421 ; $fourth : 1.333333333 ; $major-third : 1.25 ; $minor-third : 1.2 ; $major-second : 1.125 ; $minor-second : 1.066666667 ; // Base config $ms-base : 1em !default; $ms-ratio : $fifth !default; $modularscale : () !default; ================================================ FILE: app/src/components/configs/defaultDeploymentSettings.js ================================================ export default { protocol: '', port: '', server: '', username: '', password: '', askforpassword: '', passphrase: '', path: '', sftpkey: '', git: { url: '', branch: '', user: '', password: '', commitAuthor: '', commitEmail: '', commitMessage: 'Publii: update content' }, github: { server: 'api.github.com', user: '', repo: '', branch: '', token: '', parallelOperations: 1, apiRateLimiting: 1 }, gitlab: { server: 'https://gitlab.com/', rejectUnauthorized: true, repo: '', branch: '', token: '' }, google: { bucket: '', key: '', prefix: '' }, manual: { output: 'catalog', outputDirectory: '' }, netlify: { id: '', token: '' }, s3: { customProvider: false, provider: 'aws', endpoint: '', id: '', key: '', bucket: '', region: '', prefix: '', acl: 'public-read' } }; ================================================ FILE: app/src/components/configs/postEditor.config.js ================================================ export default { selector: '#post-editor', file_picker_types: 'image', contextmenu: false, plugins: "advlist autolink autosave codesample link image lists hr pagebreak searchreplace media table paste autoresize emoticons textpattern toc", toolbar1: "bold italic underline strikethrough forecolor publiilink unlink emoticons blockquote alignleft aligncenter alignright bullist numlist image gallery media table toc", toolbar2: "styleselect formatselect codesample searchreplace hr readmore undo redo restoredraft removeformat sourcecode", toolbar3: "", icons: "publii", block_formats: 'Paragraph=p;Heading 1=h1;Heading 2=h2;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Address=address;Pre=pre;Code=code;Blockquote=blockquote', extended_valid_elements: "a[*],altGlyph[*],altGlyphDef[*],altGlyphItem[*],animate[*],animateColor[*],animateMotion[*],animateTransform[*],circle[*],clipPath[*],color-profile[*],cursor[*],defs[*],desc[*],discard[*],ellipse[*],feBlend[*],feColorMatrix[*],feComponentTransfer[*],feComposite[*],feConvolveMatrix[*],feDiffuseLighting[*],feDisplacementMap[*],feDistantLight[*],feDropShadow[*],feFlood[*],feFuncA[*],feFuncB[*],feFuncG[*],feFuncR[*],feGaussianBlur[*],feImage[*],feMerge[*],feMergeNode[*],feMorphology[*],feOffset[*],fePointLight[*],feSpecularLighting[*],feSpotLight[*],feTile[*],feTurbulence[*],filter[*],font[*],font-face[*],font-face-format[*],font-face-name[*],font-face-src[*],font-face-uri[*],foreignObject[*],g[*],glyph[*],glyphRef[*],hatch[*],hatchpath[*],hkern[*],iframe[*],image[*],line[*],linearGradient[*],marker[*],mask[*],mesh[*],meshgradient[*],meshpatch[*],meshrow[*],metadata[*],missing-glyph[*],mpath[*],path[*],pattern[*],polygon[*],polyline[*],radialGradient[*],rect[*],set[*],solidcolor[*],stop[*],style[*],svg[*],switch[*],symbol[*],text[*],textPath[*],title[*],tref[*],tspan[*],unknown[*],use[*],view[*],vkern[*],publii-amp,publii-non-amp,script[*],i[*],video[*],audio[*],source[*],stream[*],input[*]", valid_children: '+a[div|p|figure|pre|h1|h2|h3|h4|h5|h6|header|footer|article|aside|section|table|blockquote|video]', formats: { alignleft: { selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left' }, aligncenter: { selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center' }, alignright: { selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right' }, alignjustify: { selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-justify' } }, preview_styles: false, resize: false, menubar: false, forced_root_block : "", force_br_newlines : false, force_p_newlines : true, paste_as_text: false, keep_styles: false, image_class_list: [ {title: 'None', value: 'post__image'}, {title: 'Full image', value: 'post__image post__image--full'}, {title: 'Wide image', value: 'post__image post__image--wide'}, {title: 'Left-aligned image', value: 'post__image post__image--left'}, {title: 'Right-aligned image', value: 'post__image post__image--right'}, {title: 'Centered image', value: 'post__image post__image--center'} ], codesample_languages: [ { text: 'Apache Configuration', value: 'apacheconf' }, { text: 'ASP.NET', value: 'aspnet' }, { text: 'Bash', value: 'bash' }, { text: 'BASIC', value: 'basic' }, { text: 'Batch', value: 'batch' }, { text: 'BBcode', value: 'bbcode' }, { text: 'C', value: 'c' }, { text: 'C++', value: 'cpp' }, { text: 'ColdFusion Script', value: 'cfscript' }, { text: 'C#', value: 'csharp' }, { text: 'C-like', value: 'clike' }, { text: 'CSS', value: 'css' }, { text: 'Dart', value: 'dart' }, { text: 'Docker', value: 'docker' }, { text: 'Elixir', value: 'elixir' }, { text: 'Elm', value: 'elm' }, { text: 'GDScript', value: 'gdscript' }, { text: 'Git', value: 'git' }, { text: 'GLSL', value: 'glsl' }, { text: 'Go', value: 'go' }, { text: 'GraphQL', value: 'graphql' }, { text: 'HAML', value: 'haml' }, { text: 'Handlebars', value: 'handlebars' }, { text: 'Haskell', value: 'haskell' }, { text: 'HTML', value: 'html' }, { text: 'HTTP', value: 'http' }, { text: 'INI', value: 'ini' }, { text: 'Java', value: 'java' }, { text: 'JavaScript', value: 'javascript' }, { text: 'JSON', value: 'json' }, { text: 'JSONP', value: 'jsonp' }, { text: 'JSX', value: 'jsx' }, { text: 'Kotlin', value: 'kotlin' }, { text: 'LaTeX', value: 'latex' }, { text: 'LESS', value: 'less' }, { text: 'Lisp', value: 'lisp' }, { text: 'Lua', value: 'lua' }, { text: 'Makefile', value: 'makefile' }, { text: 'Markdown', value: 'markdown' }, { text: 'MATLAB', value: 'matlab' }, { text: 'NASM', value: 'nasm' }, { text: 'Nginx', value: 'nginx' }, { text: 'Objective-C', value: 'objectivec' }, { text: 'Pascal', value: 'pascal' }, { text: 'Perl', value: 'perl' }, { text: 'PHP', value: 'php' }, { text: 'PowerShell', value: 'powershell' }, { text: 'Pug', value: 'pug' }, { text: 'Python', value: 'python' }, { text: 'R', value: 'r' }, { text: 'Regex', value: 'regex' }, { text: 'Ruby', value: 'ruby' }, { text: 'Rust', value: 'rust' }, { text: 'Sass', value: 'sass' }, { text: 'SCSS', value: 'scss' }, { text: 'Scala', value: 'scala' }, { text: 'SQL', value: 'sql' }, { text: 'Swift', value: 'swift' }, { text: 'Twig', value: 'twig' }, { text: 'TypeScript', value: 'typescript' }, { text: 'VB.NET', value: 'vbnet' }, { text: 'Visual Basic', value: 'visual-basic' }, { text: 'YAML', value: 'yaml' }, { text: 'XML', value: 'markup' } ], element_format : 'html', fix_list_elements : true, image_caption: true, autosave_ask_before_unload: false, autosave_interval: "10s", autosave_restore_when_empty: false, autosave_retention: "30m", entity_encoding: "raw", allow_script_urls: true, convert_urls: false, textpattern_patterns: [ {start: '*', end: '*', format: 'italic'}, {start: '**', end: '**', format: 'bold'}, {start: '##', format: 'h2'}, {start: '###', format: 'h3'}, {start: '####', format: 'h4'}, {start: '#####', format: 'h5'}, {start: '######', format: 'h6'}, {start: '1. ', cmd: 'InsertOrderedList'}, {start: '* ', cmd: 'InsertUnorderedList'}, {start: '- ', cmd: 'InsertUnorderedList'} ], toc_depth: 6, toc_header: "h3", toc_class: "post__toc", rel_list: [ {title: 'noreferrer', value: 'noreferrer'}, {title: 'nofollow', value: 'nofollow'}, {title: 'noopener', value: 'noopener'}, {title: 'sponsored', value: 'sponsored'}, {title: 'ugc', value: 'ugc'}, ], link_context_toolbar: false, link_quicklink: false, codesample_global_prismjs: true }; ================================================ FILE: app/src/components/configs/preloaderImages.js ================================================ /* * This file contains inline preloaders for the use in the post editor * * in the future it will be replaced with the SVG or CSS preloaders */ export default { gray: 'data:image/gif;base64,R0lGODlhGAAYAPe+APj5+vf4+fb3+PT19vX29+jp7Nrc4Nze4ubn6vDx89/h5O3u8ODi5fLz9O7v8cXHzfP09ezu8PHy89ja3q6xudfZ3eLk58/R1urr7uvt79bY3O/x8qGlru3v8eTl6OHj5rm8w+7w8s7Q1eXm6aapst7g483P1NHU2MLFy8nL0dTW2vLz9ezt78HEysjK0KSosdDS16uutr/CyK2wuOfo65+jrLq9xMvN07S3v5mcpp6iq8rM0t3f477Bx6irtMDDyeLj5urs7rG0vPP19qKmr8zO0+zt8Lu+xLe6wdLV2bO2vpygqZqdp+bo6vP09u/w8uHi5dnb3+Xn6amstOfp69vd4ZyfqfHz9Onq7e/x86CkrfX2+Onr7b3Axqqttba4wLS3vtze4eXn6pueqMzO1K+yuqmstfX3+Lq8w+bo6+3v8NPV2sfJz/Dy8/Hy9Ovs7vT297y/xd3e4sTHzLm7wpibpeTm6Keqs9DS1t3f4srN0tvc4OTm6dHS152hqtrb3/f5+vb4+eXm6J6hq83P1evs7/Hz9ZSYo97f452gqbW4wMDDyMjL0OPl5+Pl6NbY3eDh5La5wNTW28HDybW3vqistcDCyM/S16KlrsfKz8bIzsTGzPb3+cPFy8XIzff4+vT19+jq7PL09ePk59XX29nb3tLU2bK1vaWpsry/xqOnsLK1vNrc39PV2d7g5LCzu7u9xMbJzsrM0bi7wuLj59vd4Le5wc/S1uDh5aaqsr7Ax6SosMvN0rGzu5eapcDCycPGy6qttpmcp8bIzbS2vtbX29TX262wt+Di5ry+xdHT2e3u8ejq7cnL0NXX2ra4v7/Cx8nM0dTV2qyvt7i6wtHT2LW3v5WZpKCjrcPGzNrb4Obn6c3Q1Keqspibpt/h5a+yuerr7dDR1qGlrbG1vaWosbi6wdHU162xuaGkrd/g5NPW2q2xuNDT172/xrG1vOnq7LW4v6istNbX3K6yuaKlr6ers6uttpyfquLl56epssnK0NfY3La5wbCzutLT2SH/C05FVFNDQVBFMi4wAwEAAAAh/i1NYWRlIGJ5IEtyYXNpbWlyYSBOZWpjaGV2YSAod3d3LmxvYWRpbmZvLm5ldCkAIfkEBAoA/wAsAAAAABgAGAAACP8AAQgcKJDThia4FIgJwYmgQ4cDPuxIhqNXmVfxgB2A8NAhjUy2lAh5RWFGDC8+zNj4EKBjAB66kHwhdmrVqV7HUJJ4EWydgIcluoCYheSXKVoFCuAygYOEKiIvkjgs0OKIjVSk2jwMsSMXBy25FAwEZUJXlx55WnbkZGpXDR1gGggc0UJGjwlqOwLgxMbPEh0TAASQ1OJHNDd6CS6IMYYJGk4NbnRCUSuxwwc5cuhjwcLFg2EFLBNkJcwXPlwFXGjat0D0wBGoOOwKE0qWix0dXAs8oyZChAELyKTghUG3wABqRSm7UYSBcQEECPwMUKuICVJOXAcYwP2MQC4XRFyHyJdX7wAJDSA0BLDlD4wTkrCUdzgkxAYJHAd2MNZKRYUmoDzESRsRqOFAFj8RFEoxpGgwgStYJACBEw04gAEWQWTQQYAOBcBFKRWUYsABJTBggQcjIFAAFkZkp9cGDBgwYgkKfDCKByouwGFiAjgwAjIKMACEBzSwsMJ8iQVAQAMJ4OedXgEBACH5BAUKAFoALAAAAAAYABgAAAj/AAEIHCgwQJtQoyxQycKJoEOHoDzgyZYKhDlqqVIocPLQIRZuMuLYAIHEmpJTr4T88PCpY4APnmT0iHPExhE6OIS8ojBjVYWGDi08aPFDRqwKIzCEG3UCFoVpMWZocPgmxSYUc6JceZjlArgpUygAGUhAGpsHbIAE6AgggIZpPkjAWiGwQAoXbOS0ZNu2CIkXqAwA+KRtRwoYdPkKdEBOFREZnESZunHjm2KC0TiMQ7fAgTgTJoJcHniAQ41yFoKIEwHjyWiBdojoGGfgzQk8STa8BuAh3ZIarJ6oONEqwm4DVphg+zCklDQVUnZvypGjm5FPH0hpOABq9JM71+rQh2kYYUKFCQVGgzpHwttUAAIYlDJwwDjfAGszVHMzMMGBKge4EoQAD31CwAAEAOUQCzzwUAID2yywAhwEwCFKGw1AMIAAezm0AAMKQDGKHQiEgkEGHWwgQQMDrMXWFU1YMMoICBQQTgYRhJAABC7eJ8EbBdBgIgshXEHAbgIJMAAEThDQ40MBAQAh+QQFCgBXACwAAAAAGAAYAAAI/wABCBwoMICbIFJGcJEQiKBDhwRozNsxp4WMRZsuWBjw0GEGFZkeoPgho0ucIyBAZGrSEUCAEYRcsHnQCcXILjZAIPmCZk9DhwgI7UjhQkQYKiwySHmEwhYOJTgMOFzQp8gNMiVWPJSwJpIQIYr4DDzzR4QJESMCtAxgAEwZCi2cCHxzAsaFD2pbAggEg8IMCogAAIKU5EQFuXoFPqETI8aDQAP+qFAhNvFAEV6mxHOQYIIGDR0sD1TgxccMPh0mVIjiRrRABPdImMnjwECUPa1di5H3wp4cNwcMVHniGkCeF0TkOYLD4IAcDMUZceBArwOgNDxKNCIgegG7GjV6cI0CsEGBAgYsLHPS5EeHligCOTWBAkRQAr2cLgwaswSMoYErCOKBIDQ8MZ5DcDxgRQ5M1KOAQwkggAANBRhhCAGcBBAAIHB8cUgdieDRURsFFIBFIRGE0AYEAwgAABA6DHKBix0NYAQGGXQQggQNDEBAIIHccgAgiQUwRBsbZMHjAGfkVZxLApwhgJMdBQQAIfkEBQoAawAsAAAAABgAGAAACP8A1wgcOFDUglAFWDQIAKChw4cPt3CppYyMLBcubqiQAgqixw5RLhS5kcKFpgedWrQwEYqgy1DGYIgwUYQXr2bDUPyQoatFCYYQuRRrdeIWKSgYOiwLVSUFtC5HuvCA+KQUKRXOLDjxeGVCDxsgdKVxKIBBBQ0VsLh0mSfVLCSZhjR0YKDUhCZrXQYgZeuLLSgNxRgw4KqjR4gJfhFToicAAWQHDjA7fNjUKSHJnjRQUKJEAsoeP6x6hSNNAgYKoIgCDbHAKQq9GEgA8kEr64dUXs0ogwuCh1EeGtx2iOtYjFcIztDwMMLB8IYmpnh55pwFAgQYBAx3gMOHD2AMVxSIKIAlQV6XO1CRMBOmYYAFWIJEGHJeoKlcql7AWt0QlJEMaoQwAFAPLRPLLhwQEcwHEDnRgQMbSDCAdmdIwcoDMehQgxYvrHMYKFlI0MAABASgBirC5DDGEjrkkkR9AkAAAYkBRMCBLzkwoQMYuBBIGSdbaBfBLlaQgMYEKzz30ABhMMCCdocFBAAh+QQFCgBYACwAAAAAGAAYAAAI/wABCBwoMMCQJ2+COBAVgKBDhwIifCil4gSMC6a0FSDw0GECBhNISTvRToSJGztkSXvTUaCRAxMqaFDRKkmfkylcsElhoaHDBTyqGJhwQEqEDU+CfIPB5sGmBx8cXmHA48ABKqAeipLDBkULT/AGBmiioISrCC0BBAAy54cMbgMESrAARUGQtAWj9NjrQeCbUaO2bcEr0A2bOHFgBBBQYISdBYQHVjhiI1uCATQQIFgRWSAfGyDchYIQqkCorJ0x2EACYpQTDFwwcOzMhY41ahYIsMiQAU5nABZwKKFWIECICB1E/T4h5FSqDQCuhAjRxifeJ7BevUrRkEACCQ1mp4UNcIEdhVfqBkJoAGGA9YcBNFCYNuOHE7EDBhAIMODJwxBFTDNFDKv0RVAAWwgAQDt3bLKHHXzkEc07JPgwxQwVdNRQBiRck4MV6RDBAQeqvEACBRoo2FI13tSRAxNL6FADB0SgAktPeF2hwSzdYFODFi+wI0MtynUmgBEfsGKABQuo+FBAACH5BAUKAJwALAAAAAAYABgAAAj/AAEIHCgQEBw3DtQkGACIoEOHgTakYXDAwIQJfyC9OfPQ4YomCnhUjFJBg4okJ6Is6CgwgSAoCkrIqbIniskTMET0QRDgYRsEHoAwaIRhwxU3auxUuGCiCKERDgcUQCBIEIstD4d8EHFjB6FCAwMsKECDRgKWAACNIJPChSQCAodgwFLgCdqCJdjopSGwTYZCRgTcFbhCxIMHGgIEyNIhgqHBA8NsQqHnioAsIULAhQyACoofc4KckSChjWDOGVDIaCFGQIMGEHpyLvSjiyUpAQbolg1ZSpc4i7gAOENAAO/Bj2wcmdMGQIBAAOAMgJwABQgQF2QHAPLlwWmWAdZEiUICwsLASzoOWbnwHaKBSDi+ZJou8MCgOjkGaXLwcAMMMEIogQYCDl2QCBNj+DEDIzwgkIYCItBBQRlC4LDHQwLggckSftTAwQuVxOCFFzHMQIEiBkD3ECAKgKGFh0S8QIIPU8RAwSR8NITWClH0QIEPd5gxAyUPIOIEZwAIoIYjcuRhhwPtERQQACH5BAUKAE0ALAAAAAAYABgAAAj/AAEIHDjwDAQJCRoQAESwYcMAK1jQ8ACEgQJkYjoIcNgQ1IICCDyM+qCgxAEDBhhs4CjQiREsIEd4sMDApIFSFUpxCeAQVIcMQbBgcNDACYQEWFxN0ECqWKiGArI4UBOhDaeeCCqoaGWsA0EnEjaEGMISACAskk7A+LNFICcIDSQMKCsQUL4LIi5wEXhmgF+edAE4IWWiSC2eAggQ2BhYIIMiN/6JqsuwscBwvFKQWTAgQgQ1ZywD6LDDhaxQYXZxQDVC9IJ9mlwUwIXPlzBWogsMe+CCBQt9OXI8EF0LRacbDTihYTImxoLGbqL9aCGJ5wQdS/ywuVo2wIQeMlq0hQbQAIyOGrtMcXcYIE+PLrpMgBqoIJcWDrl2PHHYhlQqG0e0UEBDSbxAhCok4GACAwUUQIspvyAxCwhdlOCQAGsE8wIJPnhxTC+nrHIKMV8goQsPgDUEyAc2mNFhDDNQ4I8QStiSCQ2BQRAGMPH4U0YvOByxAxRzWSaAA2IogEsaG6zXUEAAIfkEBQoAWQAsAAAAABgAGAAACP8AAQgcODAAAScQBgggyLAhAAJXQrDAEIpGgTcSAjgkGABCghARMoQrgGDEKAtNrmwEEGBAAwkbOmSgiMDOKCgKGCxwGGDLAAgN2oiCQwDOigXbGJTgwcOIQwEEBhDQyHBLEFcHqhxIMNBNtQwsqW6McMBAKQYLAWjwRuIcqJUDqUyoMCECAE6z6ly78wSuQFAHNJD6EMBItxw5NvkdKEWFtFJDPmBjYsXAYoERWp1Q8YRVjSVaPFwGsCEJnhMZDIzTQcTO6CcwRIh7Y6FcDQ4HRmMwYUKcgwXoxnGINvrbjRumRHGSQUQVOQeLV8BIsUObRgOoXpAoIpanHDYuZBWDELgCFgkf0yp05wiEzQM20ggMBEJhyhRwF7gyvBJlDopNKbzBkAYzxDANBbCcMEo4GIxQARsy/NDCA7Q0xEkFq8xAwStC4EDHETYcEUcPMnhCGE8e/PDKK6coYQ0SINgQhwzcYOGXE+qkkAo15oCQSjZ4ePDWZZxsQIUFo4TSxnoDBQQAIfkEBQoAVwAsAAAAABgAGAAACP8AAQgcSDCAgIMBCCpcKDDAlgENJGTZ0GbIlYsYM14BdOBWoEAEIEoI0SEDhgUDGAIQcGGQDiArB0BoEyJCISwFCrRhiCdRnUNf4AAKEIATAUNGCtBAgCCBQgX1mOSw8gDOQk5PaAjyIGjFQENglowZdIGTSgAJBAGB0sQsgChadPjx5PYsCwYKFGy42KNGDXYLzg4k0KgEjzSAOtDjwIGRYIIY5BxgAMeRPCIv8jwe+KSKgQNu5Nh7IU/MZoFu9kQx4CCPGRL3EJwG4CZKhQkd7Mzw4UXB7AUaNExI4IDSFC8iZttRoeLPgEAPYsSg82SzkwonkkACBAARhRkUYGiMHH/lwwUYJ94IdDKJQhkwBshjHCHChIg/WwbyUSRESL81Oym0Qglk3FBEH4ERZAAOSuBgCwqPSJEBC1SEIYILKexAiGwKBbIHCF8gAYINXbSAAgqdPMCGC4SMIB8ATbABAghHxNGFDD+goKI0GTw2gAUXbLKIDC3MscM8NBAwWyBtcDGCFEG4Id8VAQEAIfkEBQoAbAAsAAAAABgAGAAACP8AAQgcKFAAC1xhBhBcyFDgigloSFjZFQGAgC2cGhIMgAuMDiY5fHGIEIDAAAgQBLBZyZJlklw6lozJIQyVmpIDGkjIAqrhmhdaauiI8YCVlDMWB0jY4KCDk4UfghHhsCvWMoYBBoRQk8FIT4GiYL1QlctUy7NshkQIgmVBAIFhzJBAtQMt2gRYChRYASAAMB8+cDjQSFAABgQIWABw8MzLFBOEFzoY4YHGGQSvYhzDFZlgAw+jPEDAVWbGKyqdBzqx8AGIBAa9KJwqkBosAwUMEjTB8WoVrdoAEpQooaDBk2RCTpkCzuzAAWQEAuxQQuxXgtSgXBkwIEYgFFtfbJGIsnu2yYRSBgYDGJIJyaxUPMivxFJBQwUGAgam0QXCRo8JVzC0mjMqkFLKEwvx0MURXUCTQhWhLNMBBlCQcssJrRTDBVYltKCLDD+gMEwzvPBShAkiwGAMM/KFYkILLXTygCYupHBDERdE0UFnoEihwg0uuCALGcrUwsUWwAXQAAsFhLKAKPIFBAAh+QQFCgBOACwAAAAAGAAYAAAI/wABCBwoUMACC3tYfTAigKBDh6JqyWD3QksNbN3o8LvykOAnC7BQEeFQQ8cSJjnqeKvWsaAGCiReqOLAgUg6KzmukcgA4FPHCjOm+CDxLloePnb2bLrTDoCALQEcelgVY8q0Ig4ePhkQgMAArgMhTJoxDRy/qC0DDIDQAMJAda8osLuAtiUAAg0kJCAAIECKV69gPbE78FObECE4bkh1SsgJwgRFdYgQIgAVakpwWIA8EE6GDCwIWKBmDQQXzgIJYOCCYcgoEEhsYEANAFSoAqEghHIHwgYf2isQIKAxIEE2G0cq0F5gZ0QBAQFgxInDxg3nLdtGjXoj0EOP71HqtogMogCKBQkCB3CT8WMOEJ8dP0VwVUJBk7rwPLVAwUaOqIegUHHAATwwwBFBHzywyQNswIBLEE9ssIAUB0xgQBU8LPBQALSkwIYLKdxgQh9JtKKCBhVMcIARdr0hjSw7iChCOydIQ8oEDCQAGQEFaGPKBTCcoEIpH0TQEGoBiOJAEG88MQR8DwUEACH5BAUKAJkALAAAAAAYABgAAAj/AAEIHCiQkwM7eeQ4UiOAoEOHThA9oDTDzB0fFHpEMfSQ4Cc+kyjEmOKDxAsiHGpoAaPgU8dABhRRmBHDi5cYlV6k9LMEE56GDvfgEFKGAh0RCtIg4MFohp8xTBJdcIgAjRIhYGBseKjG06AcdQYdGDiAzRcckQwE6giA0wUrh3RcGmgBBJJIa9ayBSDgwRcgAQQGuAACBIoEe8nCARAocJs5R2w8SkwwgAACZwBwWRSnixTKAwMMGB1AiqUuPwqBFgyhQQMBYlrIQJFhdds2EiScCTLnBwoqtgmECJFFwBU9KDqFsW0oQocsAQJoePBAxArQnIwUytBGYBo24Eu4iEz8pAAWDEMEEpDkIgUZKeM7JqBBo8CCwAILEdpxQ8SH9A5twYIggiBQwAAOjUBIESZcUAEfarhxxQYYNMIAEB4g0J1DASDQhwgwnKCCBlHsUYUcJSgAhSCIsbVAFCckMWIFURhwAA8KNHFdYme8AckfE0xgIwNpbMCJbZ8MkIAaDrgBByBsBQQAOw==', blue: 'data:image/gif;base64,R0lGODlhGAAYAKUAAESm9KTS/HS69NTq/Lze/IzG/Fyu9Oz2/Kza/HzC/MTm/JTO/GS29OTy/Pz+/FSu9KzW/Hy+9Nzy/MTi/JTK/GSy9PT2/LTa/ITC/Mzm/JzO/Gy29Eyq9KTW/HS+9Nzu/Lzi/IzK/Fyy9PT6/LTe/ITG/Mzq/JzS/Gy69EKl9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJBQApACwAAAAAGAAYAAAG/sCUcJgSLTIHB2C5JDqHBtLIQVUyARzAc5g4jKZVJieb3VK8X4fFMbqOOQ+Os2sZ1TOYhza1fD/0Qw8NB4QHCXtOWH9/exeDBw0VW0OKDwZygoMNEZNEAJYPIgAhDaUkiJ0pHCKsDxcSpQKpniIVFQ8ZDR8DcrNDtQwVH8MTvkQiDMkDAx8gxr8bKAwKyxmoswwoKBsQAxkmIs8AKAIoFSUZ6SHPDwICHn/pGSAPvgACER4echoKExMBrj1hkCBBBAZCHhD4B0JDryfjMGAw+FAACAIYO6B4yOcBhhIgExhwEgHjhZMdCiTwkCAEhRAFCmAI9wQFgpMIIHQIEEDDIIKXIUrQ3PIgBIScPE9o0ECBwj5f7UJoOHGCwkGOQ4IAACH5BAkFACgALAAAAAAYABgAhUSm9KTS/HS69NTq/IzG/Lze/Fyu9Oz2/Kza/HzC/JTO/GS29Mzm/OTy/MTm/Pz+/FSu9KzW/Hy+9Nzy/JTK/MTi/GSy9PT2/LTa/ITC/JzO/Gy29Eyq9KTW/HS+9Nzu/IzK/Lzi/Fyy9PT6/LTe/ITG/JzS/Gy69EKl9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAb+QJRwiDJQKo3RqMHQiIjQIQRzOChHj+yDAYgSE5+qVfnAPiSAtBfUEFsv1WwjTYdKGvhqJQNJQyQCdAAcXUIQAxN4HwmFUWkckIURHx8NAxZeRJCbKIeUHx6ZRIOQfSUDHwMRolClfREDsSesoxC2EBUMAwwctES3EAzCIb6/BscODA4kxVIGIiIFuQWNvs/QARUhFRDNAAsLFhAJFQUFJc0c4Au25gUYvbTfGycLXRQkJBga1V4iAic2POkUAQMGBCDiObLgwcOJE41OHIwQQcOGagAgCEjQUEA3Ih4odghgQkEGhx4yZEggQYIHA14WBBhpQoMCCiAIlCix0sMcRy8QMpgIYJMCTgIZJJxQKIrDBgkgcJ5cwHRIEAAh+QQJBQApACwAAAAAGAAYAAAG/sCUcJgyhAifw6ExWYiI0OED0mgoLYfRyDEiPKPCyKCqVGq3DoelBC59GhLy8mBBp0lRz2f/BiUeAAAPEQRpE4EAUhkDewMRiVEbExyIQwEDmBkMYESIgSkPGSaYApxQABypABgZrR2mp5SUAQqtG7CdqakEE72QuEIcD6kgvRfARMIPDwQgBAjIQ8oPFwQEF7/Ay8saJBckD9EABuSDF+cJ0Rwi7MIIFwgQHMAAIhXsiQUQ+yHZYCIMGFQIl4JDBwgdApSYB6behoAMfjEIQPEEhYidHmxAgWLDBoZDUAQ4oWEBhRARUDBA4aGlAI4EoTCgUPJkgRIlMESI4OFlGswoHCJQGHoTA4YEPDHi4sAgAs4SKUWAJBIEACH5BAkFACgALAAAAAAYABgAhUSm9KTS/HS69NTq/Lze/IzG/Fyu9Oz2/Kza/HzC/MTm/JTO/GS29OTy/Pz+/FSu9KzW/Hy+9Nzy/MTi/JTK/PT2/LTa/ITC/Mzm/JzO/Gy29Eyq9KTW/HS+9Nzu/Lzi/IzK/Fyy9PT6/LTe/ITG/Mzq/JzS/Gy69EKl9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAb+QJRwiHqALIPGwfOhhIjQ4YPjaSiVh4pIZDFEh52Bp2o9mA9b0YH0JQ3E40Y8u3XYF9AO5i22RB4AABsdBHV2F1ITewMYHQBfJw12DhiPKBkYmRNPX0IPDSIZgUUTEwoYJ51EDAyBjwkfpQGqUK6BGROxDLS1ggAWBMEbvESCGwAjBCMQxMUbzxbRs81Cz88cFggQls3PDxsU2hAP1AAP54MQ6h3UG+ffDwEcASbDvOYGBoAoJAH0Bdw6PQgR4ly1DCYyZEhgL4o5BiEYGODGQOECCiRaOWPFykBDIScuUgBRgIQAjidOaNDAilyUECBIlkyQIEIHAQJUMvgIZVAWSRIXItTskDJEwE4bGJyIIPTkxC9BAAAh+QQJBQAoACwAAAAAGAAYAIVEpvSk0vx0uvTU6vy83vyMxvxcrvTs9vys2vx8wvzE5vyUzvxktvT8/vxUrvSs1vx8vvTk8vzE4vyUyvxksvT09vy02vyEwvzM5vyczvxstvRMqvSk1vx0vvTc7vy84vyMyvxcsvT0+vy03vyExvzM6vyc0vxsuvRCpfUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG/kCUcIhykCwYj2dAABmI0KHDNBgoPZHsIWJxRIcCSalqVWoP28s3ocBgyEtsBF0RiSbQk6TtRnQcAAAbHRYHIoYiakIOFhKOHwIAXxoedochQhMEHxIEmF+LHg2jBCgbIwSpGqBEIXajIRAjFgQLrFABow0ZExa+DLetDREEEAEIFg+SwVKBAA/QJsxEzgAcDxwZ00PVGQEBJsvTgoIk4Ble2xvrACcmGRkC24LsG/ALExvTGw7sKBD4QCQQB4pfP32mQICYUKADwiiCDDiYKC4EiAIgLiQwQDBiiIn9oDAoQILEBQgdNIRYyYDCyhAGHrY6mQBCgg4CTmjQ0HKlGEwoG07YRHmiKIOjgJhtCMHgRE6VSaMEAQAh+QQJBQApACwAAAAAGAAYAAAG/sCUcJh6YDqEjGlyKTyI0OGDAppkMoPsZwt5RoWoy2R8xQ62n8Yn8fUQ3lVFxnpON+4haPjyJgQEDwAAHB4QEg0HiWxCHBAIFyQXKABfGwOIBw0GQiWOF11fUgOJFiRFARCpDKFEIgcWIwciKB0BHXmsRAEjvAsFAcAiuUQPvCMZFCfKlMNDHw4ODQsaGhTNRCDQIwvcBddDBNAOBRQUIczXE9AHCSEUBRzfKRwiGBQMBQUhq9eCgvMFSpTAEK+ZP2YoBmKYNGwQh3/zIpSIkIBhKA4YB7WKQNEDikBQADzAmBGKiAQePAhAsUHEg5cwRz788kDAShQoGDAQIcJAGUx0UQAwYLmBQYUKPH3ObPhAhE6eI4EOCQIAIfkECQUAKQAsAAAAABgAGAAABv7AlHCYekQ0F9CE0Ck9iNAhJ3QhESYTRSYzyASeUeGmc7kQCMrJ1jRoe8IoCKJsPmPZg4++AGV0IIAXCygPAAAcGwF5ehIRUhoBAR0dGwBhDBkfDQ0fYAknkSdgYUUKmw0XKRwaGicaFaREIh8HDQcPKAsLGhixUBoHwRQJFBQLIr5ED8EHEyUUISGWyUMDBxYSIQUhvdRDBCMjFiUFBY7eQiDhIxglJRjT3hnhDQIRJREc6BwHDg4TIvAlYIAOgz8HGjgkiJDAQ7xYADIcNJCCQUMPDB6GOXTBAQEhHAR4ECBAhEYihgxFGIULBYoNDDhoPJTyJK4NMBmIePCAAySHnj5ThnnAoGgFETt5+gx6cggAEUeRGuAJ1BA1ADynKpUZJggAIfkECQUAKQAsAAAAABgAGAAABv7AlHCYengoEBLhcko8iNAhpwRBXC4EAmgyUWieUSEj0IFALkoCN8NWeMKoUyBQvV637MxgUIJWNBpzHRQbHAAAHBsnensfb0IcFAsnGicMAGEMIAMfHwNgRwsaFGBhRROdHxApHCEUryKmRCKcDRIPDAUhIRGyUAsNwRQeuiGlvikGwQ0ECSUlGJjIQxkHBx8Y0L3TQyTWBwkRJY/cKRcW1hHhHtLc1SMfKB4RHhzlHAcjIxMiHh4Csbhh0DdiAQcBAlCgaCeLwwcH+mKJULhBBMMwFxxoJABpA4oNDCzK2qDRwQEDUhiorCDCEJRDAEJoxAAFV0gRBh5w2GnI0CChABrCcBDB8kFOnj4P+QJg9IFTnTyVTkPklINOmGGCAAAh+QQJBQAnACwAAAAAGAAYAIVEpvSk0vx0uvTU6vyMxvy83vxcrvTs9vys2vx8wvyUzvxktvTM5vzE5vz8/vxUrvSs1vx8vvTk8vyUyvzE4vxksvT09vy02vyEwvyczvxstvRMqvSk1vx0vvTc7vyMyvy84vxcsvT0+vy03vyExvyc0vxsuvRCpfUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG/sCTcHjaCD4lCAKiiDyI0OEmEQhwIJCLtlBQPKPC0KRUsiq3IAqlIABrFIpMpgq5XrjpBiMBDX0mE3IkCxsAABsLGWoMjB1SJB9/HxVgJyEFjAMNXwIEngRflRuYAwMBRSSpoJVEIQwDHgMPIRgJJI6sRBOxHh8mCRhOuUQPHsYXHQkREQDDRA0SEgwRHREmzkQX0RId3dfYQtrRJgICJs3gFAcSHhUm7xvgABIHBwUPJhoaocMY9QcKAGhYQBBdrg0DRIg4YMASwQoPDFa6oFDEBSEAKlQIESJiJQAUHSwMtYFjCAMPCkEx4cGBSBEYoJTsmDJiIUMaXLqcAGbDHYOfKTfcNDTAgYWYH30KFWrIEIYCIZwdWnpIIpQgACH5BAkFACkALAAAAAAYABgAAAb+wJRwmOJsEpTTSRMSPIjQIUcQoiw0p0AAAkEUnlGhqFSoXjWBDvdyQaDC40LZqsFuERcSgRCBGjAYJSUhESIcAAAcDCFseyBvQhweCYEYImEpIhAgExMEYAwRCQkRHJiREJ0TGkUeHhERYKcpBiAKGRMPDwKuDLNEBRnCJQwCKE6/Qw8ZJgMQDCjRAMlDBAMDEygbKL7UQh3XAwwbDBXeQhAfHwMVDOXnKSTqJiIiFYbeAB8NHwQc9fioJWhAsMA/EQYeTPvFIcMBgk90SUwG4cBDCEIASHxwCBOAihYbyOKgi4PJhUMARBgwwoLFBFBMcjw57cSEAw5G6LRAIUwfIpqIPjjImfMATI+Jkuob6sACCQPJEEkdcGCVLCJBAAAh+QQJBQApACwAAAAAGAAYAAAG/sCUcJjiVDylQiiEQXGI0CFnkyglQxTKQnPCPKPChyeBwVy13ICaATZ4IglyshTaagKdDsQDfaA8bwkoIgCFHCIleBAQFyhDACgCAoAPYCkGJ4wICJUpIhsooV+WHAEXFwQhKQAbDCgbo5YpDxckBAQPDwy7IrJQJbcECQYMFQyxvg8EExMnIs8VAL5QEMwEzyKd00MnGRMKIgbZ20QaGee5ueRDFybe6Q/S5AAZAwMQHLkcyLIR9gMlAOx7wEGeLwATPnwYUGnfPoOyAkhQCEGIwIIFZQEI0KAjw0cFCwEgSASAhwwHOn6IAEVkoQkjMiCAQKDBgZsdVUURycCBIE8HI4JawJng4IWfI4COuHlBmywR5hqMsNBgAgWnQ4IAACH5BAkFACkALAAAAAAYABgAAAb+wJRwmOKIUBFMCeNhcIjQIYAhiCQSpVIhRKFEnlHhAyXwWBPKLWXBZoQfG7LHfMWoFxrNCQXlbBgoZCgPHACGIhF4JwEBbkJTgHFgUQYLAR0QAWAPFQyek2EACxCYBSkAIhUiTmFQD5gXCIQitA+tUQkXuh4PBrSgtykPuiQaD8cPAMFQAQQEEMi2y0QLziDIwMsaEyATHITZwR0TExcc54XTjxMZEx0A6MrqHhn1GPCG8sscBAP1tvgMTdMwoOAJKfk0hDt14sMHf9JOGdLg4EABUAA8KGjgcICHKCEciBxxYAKpfgc4OjQVBcMIkQ5GyDxAs4HNARFuiSDwUuYSCAs0U0KbVmlCA5ofCIQwECYIACH5BAkFACkALAAAAAAYABgAAAb+wJRwmAIYGIJIIoKqcIjQIUCEEng8ykSpVPA8o0IOA0W2KjGYUqgQEoEfDMamfFVyQxTKYgPliCpxGwwPAIVGHngLCxoMUiKPDE5gKQ8FiycaXw8PjyJfkxwhGgEBCUUPBiIGn5OUGh0dAQ8cm5utUB4dEBACtJsAt0QPuwghtLTAwUMaCBcBHNCsyiEX1dHSwRQXBBfRycpCJwQECIaF4Ckc4yAa5t/BKBPypoa0yhwXCvIPRQAeBwPegVmQoaCGIRAcKMyATQgADQMKTuAnpIRChQ0KsPI3YYBHEwKgLFBoYcSIBhNiXRjwoaVHDGAwHHBgcsSBAw1ySnDpoZUZgQsjLNy8mbPBB1nKRFCY8AHnAAIFKEIJAgAh+QQJBQAnACwAAAAAGAAYAIVEpvSk0vx0uvTU6vyMxvy83vxcrvTs9vys2vx8wvyUzvxktvTE5vz8/vxUrvSs1vx8vvTk8vyUyvxksvT09vy02vyEwvyczvxstvTM5vxMqvSk1vx0vvTc7vyMyvzE4vxcsvT0+vy03vyExvyc0vxsuvTM6vxCpfUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG/sCTcHgCOECLkhID0hCfQ+NkgVEKOBxIAuOECjUg5IKqxGoTFpBXcwxPkyXB2TIaLZ5GhyHcBACKDhh0IwQeakJ5DnpdUBoWhR4EXRpsin9eiCMeEhIQRZQOlJhEGhIKpxoAlKKjRCUXFyQlq6mtpLABI36qtk8SASQXqn69RCMbDwG7l8UnBA8PG7u1zRIIyQIIHSF3xQAIFRUSFw3lD80lBeEcIOUNFA69AA8FIiLxBeUhGcyYEgUAJQgxcCBEgxAfGOFRwODDh3tDLIQIQSFEhxEKAZSokOFDQwy+QhwYeaBDARIkHmQYMCBDRwteEpA8EKFmhw4sWzIQMMpAF4UING3ebBkgni0DHgoMuJnhAQGjT4IAACH5BAkFACkALAAAAAAYABgAAAb+wJRwmAJwHiJRpSJ6AIjQofHxMCQZWBRKxIlKqdSkCLvRCgQPr/EITlYYqLLHExFB1xw251nkiAR0HglpUnl5fFEcAhEJGAldRUaSXlKCGBgoQgCbe5REHJclJV2bm55QDAUhBQwaBSKIp0IcJSEhEQcODg2QskIYFBQhug4HvkQRCwsUI7oNx0MJGgsaB80HsbIlGicUEw4WIwzQACcBASULI+sB0AwQ51vhBweEpwAaEB0daSQHFgcmZItSAAEECCWEGGhA7wCBXncKkLhwAUKvBPQaNBiAIRYAFBAIULyQiUgIjRo/DEAwLQCBCSAIECDhwUuCDw0+6BwwIIMgzwkwLwjw9ACCTpU8MyhQAIKCPaIFJ5jIQKADhqdDggAAIfkECQUAJwAsAAAAABgAGACFRKb0pNL8dLr01Or8jMb8vN78XK707Pb8rNr8fML8lM78zOb8ZLb05PL8xOb8/P78VK70rNb8fL703PL8lMr8xOL8ZLL09Pb8tNr8hML8nM78TKr0pNb8dL703O78jMr8vOL8XLL0bLr09Pr8tN78hMb8nNL8QqX1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABv7Ak3B4AgA2kKQSQGwOjRsk0hCqWiBMpxAaVUKsjDBEy40ik2CGSDRuGqHvLVK9FrRPocTbqD1tLCICAiIbQgUPFVh9T4EdHQx4D5IHd4sbAhKZGwqSDwtZi0J5CQkhC50CoUQbCRkZAg0XIxeFqkMSJRkJI7wetkQdJSUEvCO+v0ICHwQEHiMHDaC2CR8fJRUH2RbIABTeEhTZBybIFgrnIiEN0B6Vfd0mGhqFGA32BdJaCQHxEkIQHuxNwFDLCYAMHALwKyhhQgMPHiokKFiEgYkIGDlsI9IM4oABFThQ+KAAg0kMCCKI0NJhgYcBC2JWqFCgJsoIkPpACLAA5hcCBzMLkMBAgKLOEhwKVACBQYMEd0KCAAAh+QQJBQApACwAAAAAGAAYAAAG/sCUcJgCADjIB0cJIDqJRmTy8TBQm8+hMTqtir6crCa05Ry7okqF8XCWHI7S1ig8PtIMxiYsFB3gDhtZdSJ5KChYBIAHgoNFDIcoIikiIyNwEY5DHBsoAgIAC5YWE1ialAIeHiITliOZp3WqESgSFiMHfLEpAgkJHgcWBx+7QygJGAkHywPFQijJGB/LH6axHiUFCQQN3RXFAAUFJR4h3Q0axSIh7AwG5wNtpwAlFBQhYRASHx8k1lkRFiyg4EHIgwH8PkDQ5QRAhBMaNFDQFWGAxQEgIlgDwIBCgAAQvxEpcTFDBhABxlHoAAFCB5AosngwaXICCAIESFxA0PIEHANHDzQomEAU54WjEErI0/QAQ4ALOSEs8MBwSBAAIfkECQUAKQAsAAAAABgAGAAABv7AlHA4BAA4SI6RyByKCBGjMfl4KJvD0sihkCqpVQ524Sg7UF5OtWoQiYmY0basIU4fbZF7+DhsRxYYAFgAD3oVIoMpJHIjBwxYRQYVDAwiKQYHFoARkXaVDBsAFAd+IIqeQg8bKCgPIKUHHqlEHKwoFR+lDW+0QgwCrQ3DA75EDB4eAsMNGcZDDBEeEQPEqL4oCdIXH92XxgAJGAkoId0fC88GJeIiDwMfAxkPvuEFJSViHQP8CNdYAkIUKIBCVQZ5GTr0YgLAA4UQIfIN8XAwwwQCHq4BqFBAw4KH34ZgUGDx4oUFJTAUOBHghEcKGwCCmHCRAIkLECB0aOmxghunBxQIgCBw4QKCnC0xLIz0IMICCDgDhHCFJQgAO1pLS0ZrNFJPdlJ0cDZyWm5DeXhwUWY3aUpSN09qSFBJbEdWOUxOMnY1b0VjM1J1clNvcHBZczc5N1RhS3lLTmw=' }; ================================================ FILE: app/src/components/configs/s3ACLs.js ================================================ export default { "public-read": 'Public read', "private": 'Private', "bucket-owner-read": 'Bucket owner read', "bucket-owner-full-control": 'Bucket owner full control', "authenticated-read": "Authenticated read" }; ================================================ FILE: app/src/components/configs/s3Regions.js ================================================ export default { "": 'Select region', "us-east-1": 'US East (N. Virginia) (us-east-1)', "us-east-2": 'US East (Ohio) (us-east-2)', "us-west-1": 'US West (N. California) (us-west-1)', "us-west-2": 'US West (Oregon) (us-west-2)', "ca-central-1": 'Canada (Central) (ca-central-1)', "ca-west-1": 'Canada West (Calgary) (ca-west-1)', "eu-central-1": 'Europe (Frankfurt) (eu-central-1)', "eu-central-2": 'Europe (Zurich) (eu-central-2)', "eu-south-1": 'Europe (Milan) (eu-south-1)', "eu-south-2": 'Europe (Spain) (eu-south-2)', "eu-west-1": 'Europe (Ireland) (eu-west-1)', "eu-west-2": 'Europe (London) (eu-west-2)', "eu-west-3": 'Europe (Paris) (eu-west-3)', "eu-north-1": 'Europe (Stockholm) (eu-north-1)', "af-south-1": 'Africa (Cape Town) (af-south-1)', "ap-east-1": 'Asia Pacific (Hong Kong) (ap-east-1)', "ap-east-2": 'Asia Pacific (Taipei) (ap-east-2)', "ap-northeast-1": 'Asia Pacific (Tokyo) (ap-northeast-1)', "ap-northeast-2": 'Asia Pacific (Seoul) (ap-northeast-2)', "ap-northeast-3": 'Asia Pacific (Osaka) (ap-northeast-3)', "ap-southeast-1": 'Asia Pacific (Singapore) (ap-southeast-1)', "ap-southeast-2": 'Asia Pacific (Sydney) (ap-southeast-2)', "ap-southeast-3": 'Asia Pacific (Jakarta) (ap-southeast-3)', "ap-southeast-4": 'Asia Pacific (Melbourne) (ap-southeast-4)', "ap-southeast-5": 'Asia Pacific (Malaysia) (ap-southeast-5)', "ap-south-1": 'Asia Pacific (Mumbai) (ap-south-1)', "ap-south-2": 'Asia Pacific (Hyderabad) (ap-south-2)', "sa-east-1": 'South America (São Paulo) (sa-east-1)', "cn-north-1": 'China (Beijing) (cn-north-1)', "cn-northwest-1": 'China (Ningxia) (cn-northwest-1)', "me-south-1": 'Middle East (Bahrain) (me-south-1)', "me-central-1": "Middle East (UAE) (me-central-1)", "mx-central-1": 'Mexico (Central) (mx-central-1)', "il-central-1": 'Israel (Tel Aviv) (il-central-1)', "us-gov-east-1": 'AWS GovCloud (US-East) (us-gov-east-1)', "us-gov-west-1": 'AWS GovCloud (US) (us-gov-west-1)' }; ================================================ FILE: app/src/components/configs/sidebar-icons.js ================================================ export default { DEFAULT: '', NO_CONFIG: '', PREPARING: '', PREPARED: '', NOT_PREPARED: '', SYNCING: '', SYNCED: '', NOT_SYNCED: '', PROVIDE_ACCESS: '', SYNC: '' }; ================================================ FILE: app/src/components/mixins/BackToTools.js ================================================ export default { methods: { goBack: function() { let siteName = this.$route.params.name; this.$router.push('/site/' + siteName + '/tools/'); } } }; ================================================ FILE: app/src/components/mixins/CollectionCheckboxes.js ================================================ export default { computed: { anyCheckboxIsSelected () { return !!this.selectedItems.length; } }, methods: { toggleAllCheckboxes (useArrayIndexAsID = false) { if(this.selectedItems.length > 0 && this.selectedItems.length >= this.items.length) { this.selectedItems = []; } else { this.selectedItems = []; if (!useArrayIndexAsID) { for (let item of this.items) { this.selectedItems.push(item.id); } } else { for (let i = 0; i < this.items.length; i++) { this.selectedItems.push(i); } } } }, isChecked (id) { return this.selectedItems.indexOf(id) > -1; }, toggleSelection (id) { let index = this.selectedItems.indexOf(id); if(index > -1) { this.selectedItems.splice(index, 1); } else { this.selectedItems.push(id); } }, getSelectedItems (itemsCanBeFiltered = true) { let visibleIDs; let selectedItems; if(itemsCanBeFiltered) { visibleIDs = this.items.map(item => item.id); selectedItems = this.selectedItems.filter(id => visibleIDs.indexOf(id) > -1); } else { selectedItems = this.selectedItems; } return selectedItems; } } }; ================================================ FILE: app/src/components/mixins/GoToLastOpenedWebsite.vue ================================================ ================================================ FILE: app/src/components/mixins/PostEditorsCommon.vue ================================================ ================================================ FILE: app/src/components/post-editor/AuthorPopup.vue ================================================ ================================================ FILE: app/src/components/post-editor/CodeMirror/codemirror-4.inline-attachment.js ================================================ /* * CodeMirror version for inlineAttachment for Publii * * Call inlineAttachment.attach(editor) to attach to a codemirror instance * * It is a modified version of the codemirror-4.inline-attachment.js file * Original Author: Roy van Kaathoven * Contact: ik@royvankaathoven.nl * * License: MIT * * Copyright (c) 2013 Roy van Kaathoven * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function() { 'use strict'; var codeMirrorEditor = function(instance) { if (!instance.getWrapperElement) { throw "Invalid CodeMirror object given"; } this.codeMirror = instance; }; codeMirrorEditor.prototype.getValue = function() { return this.codeMirror.getValue(); }; codeMirrorEditor.prototype.insertValue = function(val) { this.codeMirror.replaceSelection(val); }; codeMirrorEditor.prototype.setValue = function(val) { var cursor = this.codeMirror.getCursor(); this.codeMirror.setValue(val); this.codeMirror.setCursor(cursor); }; codeMirrorEditor.attach = function(codeMirror, options) { options = options || {}; var editor = new codeMirrorEditor(codeMirror), inlineattach = new inlineAttachment(options, editor), el = codeMirror.getWrapperElement(); codeMirror.setOption('onDragEvent', function(data, e) { if (e.type === "drop") { e.stopPropagation(); e.preventDefault(); return inlineattach.onDrop(e); } }); }; var codeMirrorEditor4 = function(instance) { codeMirrorEditor.call(this, instance); }; codeMirrorEditor4.attach = function(codeMirror, options) { options = options || {}; var editor = new codeMirrorEditor(codeMirror), inlineattach = new inlineAttachment(options, editor), el = codeMirror.getWrapperElement(); codeMirror.on('drop', function(data, e) { if (inlineattach.onDrop(e)) { e.stopPropagation(); e.preventDefault(); return true; } else { return false; } }); }; inlineAttachment.editors.codemirror4 = codeMirrorEditor4; })(); ================================================ FILE: app/src/components/post-editor/CodeMirror/inline-attachment.js ================================================ /* * Inline Text Attachment for Publii in CodeMirror * * It is a modified version of the inline-attachment.js file * Original Author: Roy van Kaathoven * Contact: ik@royvankaathoven.nl * * License: MIT * * Copyright (c) 2013 Roy van Kaathoven * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function(document, window) { 'use strict'; var inlineAttachment = function(options, instance) { this.settings = inlineAttachment.util.merge(options, inlineAttachment.defaults); this.editor = instance; this.filenameTag = '{filename}'; this.lastValue = null; }; inlineAttachment.editors = {}; inlineAttachment.util = { merge: function() { var result = {}; for (var i = arguments.length - 1; i >= 0; i--) { var obj = arguments[i]; for (var k in obj) { if (obj.hasOwnProperty(k)) { result[k] = obj[k]; } } } return result; }, appendInItsOwnLine: function(previous, appended) { return (previous + "\n\n[[D]]" + appended) .replace(/(\n{2,})\[\[D\]\]/, "\n\n") .replace(/^(\n*)/, ""); }, insertTextAtCursor: function(el, text) { var scrollPos = el.scrollTop, strPos = 0, browser = false, range; if ((el.selectionStart || el.selectionStart === '0')) { browser = "ff"; } else if (document.selection) { browser = "ie"; } if (browser === "ie") { el.focus(); range = document.selection.createRange(); range.moveStart('character', -el.value.length); strPos = range.text.length; } else if (browser === "ff") { strPos = el.selectionStart; } var front = (el.value).substring(0, strPos); var back = (el.value).substring(strPos, el.value.length); el.value = front + text + back; strPos = strPos + text.length; if (browser === "ie") { el.focus(); range = document.selection.createRange(); range.moveStart('character', -el.value.length); range.moveStart('character', strPos); range.moveEnd('character', 0); range.select(); } else if (browser === "ff") { el.selectionStart = strPos; el.selectionEnd = strPos; el.focus(); } el.scrollTop = scrollPos; } }; inlineAttachment.defaults = { uploadFieldName: 'file', defaultExtension: 'png', jsonFieldName: 'filename', allowedTypes: [ 'image/jpeg', 'image/png', 'image/jpg', 'image/gif', 'image/svg+xml', 'image/webp' ], progressText: '![Uploading file...]()', urlText: "![Image description]({filename})", errorText: "Error uploading file", extraParams: {}, extraHeaders: {}, beforeFileUpload: function() { return true; }, onFileReceived: function() {}, onFileUploadResponse: function() { return true; }, onFileUploaded: function() {} }; inlineAttachment.prototype.uploadFile = async function(file) { let postID = parseInt(document.querySelector('.post-editor-markdown').getAttribute('data-post-id'), 10); mainProcessAPI.send('app-image-upload', { 'id': postID, 'site': window.app.getSiteName(), 'path': await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(file)) }); mainProcessAPI.receiveOnce('app-image-uploaded', (data) => { var newValue = ''; if (data.baseImage.size) { newValue = this.settings.urlText.replace(this.filenameTag, data.baseImage.url + ' =' + data.baseImage.size[0] + 'x' + data.baseImage.size[1]); } else { newValue = this.settings.urlText.replace(this.filenameTag, data.baseImage.url); } var text = this.editor.getValue().replace(this.lastValue, newValue); this.editor.setValue(text); }); }; inlineAttachment.prototype.isFileAllowed = function(file) { if (file.kind === 'string') { return false; } if (this.settings.allowedTypes.indexOf('*') === 0){ return true; } else { return this.settings.allowedTypes.indexOf(file.type) >= 0; } }; inlineAttachment.prototype.onFileUploadResponse = function(xhr) { if (this.settings.onFileUploadResponse.call(this, xhr) !== false) { var result = JSON.parse(xhr.responseText), filename = result[this.settings.jsonFieldName]; if (result && filename) { var newValue; if (typeof this.settings.urlText === 'function') { newValue = this.settings.urlText.call(this, filename, result); } else { newValue = this.settings.urlText.replace(this.filenameTag, filename); } var text = this.editor.getValue().replace(this.lastValue, newValue); this.editor.setValue(text); this.settings.onFileUploaded.call(this, filename); } } }; inlineAttachment.prototype.onFileInserted = function(file) { if (this.settings.onFileReceived.call(this, file) !== false) { this.lastValue = this.settings.progressText; this.editor.insertValue(this.lastValue); } }; inlineAttachment.prototype.onDrop = function(e) { var result = false; for (var i = 0; i < e.dataTransfer.files.length; i++) { var file = e.dataTransfer.files[i]; if (this.isFileAllowed(file)) { result = true; this.onFileInserted(file); this.uploadFile(file); } } return result; }; window.inlineAttachment = inlineAttachment; })(document, window); ================================================ FILE: app/src/components/post-editor/DatePopup.vue ================================================ ================================================ FILE: app/src/components/post-editor/EasyMde.vue ================================================ ================================================ FILE: app/src/components/post-editor/Editor.vue ================================================ ================================================ FILE: app/src/components/post-editor/EditorBridge.js ================================================ import EditorConfig from './../configs/postEditor.config.js'; import Utils from './../../helpers/utils'; class EditorBridge { constructor(itemID, itemType = 'post') { this.itemID = itemID; this.itemType = itemType; this.tinyMCECSSFiles = this.getTinyMCECSSFiles(); this.customThemeEditorConfig = this.getCustomThemeEditorConfig(); this.tinymceEditor = false; this.callbackForTinyMCE = false; this.postEditorInnerDragging = false; this.contentImageUploading = false; this.init(); } updateItemID (newItemID) { this.itemID = newItemID; let contentToUpdate = this.tinymceEditor.getContent().replace(/media\/posts\/temp/gmi, 'media/posts/' + this.itemID + '/'); this.tinymceEditor.setContent(contentToUpdate); } init() { let customFormats = this.loadCustomFormatsFromTheme(); let editorConfig = Object.assign({}, EditorConfig, { setup: this.setupEditor.bind(this, customFormats), file_picker_callback: this.filePickerCallback.bind(this), content_css: this.tinyMCECSSFiles, style_formats: customFormats, statusbar: true, browser_spellcheck: window.app.spellcheckerIsEnabled() }); if (window.app.wysiwygAdditionalValidElements() !== '') { let additionalValidElements = window.app.wysiwygAdditionalValidElements(); editorConfig.extended_valid_elements = editorConfig.extended_valid_elements + ',' + additionalValidElements; } if (window.app.wysiwygCustomElements() !== '') { let customElements = window.app.wysiwygCustomElements(); editorConfig.custom_elements = customElements; } // Remove style selector when there is no custom styles from the theme if(customFormats.length === 0) { editorConfig.toolbar2 = editorConfig.toolbar2.replace('styleselect', ''); } editorConfig = Utils.deepMerge(editorConfig, window.app.tinymceCustomConfig()); if(this.customThemeEditorConfig) { editorConfig = Utils.deepMerge(editorConfig, this.customThemeEditorConfig); } if (window.app.getWysiwygTranslation()) { tinymce.addI18n('custom', window.app.getWysiwygTranslation()); editorConfig.language = 'custom'; } tinymce.init(editorConfig); } focus () { this.tinymceEditor.focus(); } setupEditor(customFormats, editor) { let self = this; this.tinymceEditor = editor; this.addEditorButtons(); editor.on('init', async () => { $('.tox-tinymce').append($('
    Drag image here
    ')); $('.tox-tinymce').addClass('is-loaded'); this.initEditorDragNDropImages(editor); // Scroll the editor to bottom in order to avoid issues // with the text under gradient let iframe = document.getElementById('post-editor_ifr'); if (document.getElementById('app').classList.contains('use-wide-scrollbars')) { iframe.contentWindow.document.documentElement.classList.add('use-wide-scrollbars'); } iframe.contentWindow.window.document.body.addEventListener("keydown", function(e) { let selectedNode = $(editor.selection.getNode()); let selectedNodeHeight = selectedNode.outerHeight(); if(selectedNodeHeight > iframe.contentWindow.window.outerHeight * .75) { selectedNodeHeight = 0; } let cursorPos = selectedNode.position().top + selectedNodeHeight; let iframeContentHeight = iframe.contentWindow.window.document.body.scrollHeight; if(cursorPos > iframeContentHeight - 150) { iframe.contentWindow.scrollTo(0, iframeContentHeight); } }, false); // Handle Enter key in figcaption to exit figure iframe.contentWindow.window.document.body.addEventListener("keydown", function (e) { if (e.keyCode === 13 && !e.shiftKey) { // on enter, but when shift is not pressed let node = editor.selection.getNode(); if (node.tagName === 'FIGCAPTION' || node.parentNode.tagName === 'FIGCAPTION') { let figcaption = node.tagName === 'FIGCAPTION' ? node : node.parentNode; let figure = figcaption.closest('figure'); if (figure) { e.preventDefault(); e.stopPropagation(); // check if next element is paragraph - then focus on in, instead of creating a new paragraph let next = figure.nextElementSibling; if (next && next.tagName === 'P') { let range = editor.dom.createRng(); range.setStart(next, 0); range.collapse(true); editor.selection.setRng(range); } else { editor.selection.select(figure); editor.selection.collapse(false); editor.execCommand('mceInsertContent', false, '

    '); } return false; } } } }, true); // DblClick on IMG opens the dialog iframe.contentWindow.window.document.body.addEventListener("dblclick", function (e) { if (e.target.tagName === 'IMG') { const figure = e.target.closest('figure'); if (figure) { // Ensure selection/handles work as expected if (figure.getAttribute('contenteditable') === 'false') { figure.removeAttribute('contenteditable'); setTimeout(() => figure.setAttribute('contenteditable', 'false'), 100); } // Copy post__image* classes from FIGURE to IMG for better preselect const figcaption = figure.querySelector('figcaption'); if (figcaption) { const figureClasses = Array.from(figure.classList).filter(cls => cls.startsWith('post__image')); figureClasses.forEach(cls => { if (!e.target.classList.contains(cls)) { e.target.classList.add(cls); } }); } } tinymce.activeEditor.execCommand('mceImage'); } }, false); // ---------------------- Image dialog handling (scoped) ---------------------- const LAYOUT_CLASSES = [ 'post__image--full', 'post__image--wide', 'post__image--center', 'post__image--left', 'post__image--right' ]; // State keyed by a stable token assigned on open const preDialogCustomImgOnly = new Map(); // token -> string[] (custom classes originally on IMG) const preDialogImgLayout = new Map(); // token -> { removed: string[], injected: string|null } const customImageClasses = new Map(); // token -> string[] (persist custom classes for Save) let activeToken = null; let dialogWasConfirmed = false; const makeToken = () => 'publii-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8); const findNodesByToken = (doc, token) => { const figure = doc.querySelector(`figure[data-publii-token="${token}"]`); const img = figure ? figure.querySelector('img') : null; return { figure, img, figcaption: figure ? figure.querySelector('figcaption') : null }; }; const getLayoutFrom = (el) => { if (!el) return null; const cls = Array.from(el.classList); return cls.find(c => LAYOUT_CLASSES.includes(c)) || null; }; const stripLayouts = (el) => { if (!el) return; LAYOUT_CLASSES.forEach(c => el.classList.remove(c)); }; editor.on('BeforeExecCommand', function (e) { if (e.command !== 'mceImage') return; const selNode = editor.selection.getNode(); if (!selNode || selNode.tagName !== 'IMG') return; const img = selNode; const figure = img.closest('figure'); // Assign token ONLY to existing figure (never to IMG) activeToken = makeToken(); if (figure) figure.setAttribute('data-publii-token', activeToken); dialogWasConfirmed = false; // Snapshot current IMG classes (keep original layout info always) const imgClasses = Array.from(img.classList); const imgLayoutNow = imgClasses.filter(c => LAYOUT_CLASSES.includes(c)); const imgCustomNow = imgClasses.filter(c => !c.startsWith('post__image')); // Store custom IMG classes and temporarily remove them for cleaner dialog UI if (imgCustomNow.length) { preDialogCustomImgOnly.set(activeToken, imgCustomNow.slice()); customImageClasses.set(activeToken, imgCustomNow.slice()); imgCustomNow.forEach(c => img.classList.remove(c)); } else { customImageClasses.set(activeToken, []); } // Merge custom classes from FIGURE (keep them for Save) if (figure) { const figCustom = Array.from(figure.classList).filter(c => !c.startsWith('post__image')); if (figCustom.length) { const prev = customImageClasses.get(activeToken) || []; customImageClasses.set(activeToken, [...new Set([...prev, ...figCustom])]); } } // Preselect layout: // - if FIGURE exists -> prefer FIGURE's layout and avoid duplicates on IMG // - if FIGURE does NOT exist -> DO NOT strip existing IMG layout (keep preselect intact) const layoutToUse = getLayoutFrom(figure) || (imgLayoutNow.length ? imgLayoutNow[0] : null); let injected = null; if (figure) { // Figure present: remove all IMG layouts to avoid dupes, inject figure's layout for dialog preselect imgLayoutNow.forEach(c => img.classList.remove(c)); if (layoutToUse && !img.classList.contains(layoutToUse)) { img.classList.add(layoutToUse); injected = layoutToUse; } } else { // No figure yet: leave IMG layouts as-is; no injection injected = null; } preDialogImgLayout.set(activeToken, { removed: imgLayoutNow, // original layout classes on IMG (may be empty if none) injected // what we temporarily added to IMG (or null) }); }); // Mark that dialog confirmed a change (we'll apply it in CloseWindow to avoid races) editor.on('ExecCommand', function (e) { if (e.command === 'mceImage') { dialogWasConfirmed = true; } }); // Close dialog — single place to handle both Save and Cancel deterministically editor.on('CloseWindow', function () { setTimeout(() => { const doc = iframe.contentWindow.window.document; // Resolve nodes by token; if not found (node replaced), fallback to selection const resolveByTokenOrSelection = () => { let { figure, img } = findNodesByToken(doc, activeToken); if (!img) { let selNode = editor.selection.getNode(); if (selNode && selNode.nodeType === 1) { if (selNode.tagName === 'IMG') { img = selNode; figure = img.closest('figure'); } else if (selNode.closest) { const f = selNode.closest('figure'); if (f) { figure = f; img = f.querySelector('img'); } } } if (!figure && img && img.closest) figure = img.closest('figure'); } return { figure, img }; }; try { const { figure, img } = resolveByTokenOrSelection(); const hasFigure = !!figure; const snap = preDialogImgLayout.get(activeToken) || { removed: [], injected: null }; const origCustomImg = preDialogCustomImgOnly.get(activeToken) || []; const storedCustom = customImageClasses.get(activeToken) || []; // FIX: treat first insert as confirmed if figure exists and IMG holds layout/post__image if (!dialogWasConfirmed && hasFigure && img) { // if IMG has layout/post__image but FIGURE doesn't → user intended to save const imgHasLayout = LAYOUT_CLASSES.some(c => img.classList.contains(c)); const figHasLayout = LAYOUT_CLASSES.some(c => figure.classList.contains(c)); const imgHasPost = img.classList.contains('post__image'); const figHasPost = figure.classList.contains('post__image'); if ((imgHasLayout && !figHasLayout) || (imgHasPost && !figHasPost)) { dialogWasConfirmed = true; } } // ---- CANCEL ---- if (!dialogWasConfirmed) { if (img) { // Remove injected layout and restore original IMG layout (only if we actually removed/injected) if (snap.injected) img.classList.remove(snap.injected); if (snap.removed && snap.removed.length) { snap.removed.forEach(c => { if (!img.classList.contains(c)) img.classList.add(c); }); } // Restore custom classes on IMG if (origCustomImg.length) { origCustomImg.forEach(c => { if (!img.classList.contains(c)) img.classList.add(c); }); } } // Do not touch FIGURE on Cancel } else { // ---- SAVE ---- if (hasFigure && img) { // Classes after dialog const imgClsNow = Array.from(img.classList); const figClsNow = Array.from(figure.classList); // Layout chosen in dialog; null means "None" let newLayout = imgClsNow.find(c => LAYOUT_CLASSES.includes(c)) || null; // Detect explicit "None" when we had injected a layout into IMG // (BeforeExecCommand sets snap.injected for figure edits) const userSelectedNone = (newLayout === null) && (snap && snap.injected); // Fallback only if no explicit "None" and we previously removed layout from IMG if (!newLayout && !userSelectedNone && snap && snap.removed && snap.removed.length) { newLayout = snap.removed[0]; // restore previous layout (e.g., --wide) } // Strip all layout classes first stripLayouts(img); stripLayouts(figure); // Merge non-layout classes for FIGURE const nonLayoutImg = imgClsNow.filter(c => !LAYOUT_CLASSES.includes(c)); const nonLayoutFigure = figClsNow.filter(c => !LAYOUT_CLASSES.includes(c)); // Include stored custom classes captured before dialog const merged = new Set([ ...nonLayoutFigure, ...nonLayoutImg, ...storedCustom ]); if (!merged.has('post__image')) merged.add('post__image'); figure.className = Array.from(merged).join(' '); // Apply exactly one layout (if any) on FIGURE if (newLayout) figure.classList.add(newLayout); // IMG must be clean in caption mode img.removeAttribute('class'); } else if (img && !hasFigure) { // Plain IMG (no caption) on Save: restore custom classes we removed for dialog if (origCustomImg.length) { origCustomImg.forEach(c => { if (!img.classList.contains(c)) img.classList.add(c); }); } // Layout left as TinyMCE set it on IMG (we don't interfere here) } } // Cleanup tokens and state (remove from FIGURE if present; we never add token to IMG) if (figure && figure.removeAttribute) figure.removeAttribute('data-publii-token'); preDialogImgLayout.delete(activeToken); preDialogCustomImgOnly.delete(activeToken); customImageClasses.delete(activeToken); activeToken = null; dialogWasConfirmed = false; // ---------- SAFE, LOCAL post-cleanup ---------- try { // anchor: edited figure OR

    OR the itself const anchorEl = (hasFigure && figure) || (img && img.closest && img.closest('p')) || img; const nextEl = anchorEl && anchorEl.nextElementSibling; if (nextEl && nextEl.tagName === 'P') { // Keep paragraph if it contains any media const hasMedia = nextEl.querySelector('img,figure,video,iframe,svg,picture,source,canvas,audio'); // Visible text check (strip non-breaking spaces) const text = nextEl.textContent.replace(/\u00a0/g, '').trim(); // Remove only TinyMCE fillers from HTML and re-check emptiness const cleanedHTML = nextEl.innerHTML .replace(/ /gi, '') .replace(/]*data-mce-bogus="1"[^>]*>/gi, '') .trim(); // Remove paragraph ONLY if it has no media AND no visible text AND no real HTML if (!hasMedia && text === '' && cleanedHTML === '') { nextEl.remove(); } } } catch (_) { } // --------------------------------------------- } catch (error) { console.warn('Error during image dialog close handling:', error); // Best-effort cleanup preDialogImgLayout.delete(activeToken); preDialogCustomImgOnly.delete(activeToken); customImageClasses.delete(activeToken); activeToken = null; dialogWasConfirmed = false; } }, 80); // small delay to ensure dialog DOM changes are committed }); // ---------------------- END image dialog handling ---------------------- // Support for dark mode let htmlElement = iframe.contentWindow.window.document.querySelector('html'); htmlElement.setAttribute('data-theme', await window.app.getCurrentAppTheme()); htmlElement.setAttribute('style', window.app.overridedCssVariables()); // Add inline editors this.addInlineEditor(customFormats); this.addLinkEditor(iframe); this.tinymceEditor.once('keyup', e => { window.app.reportPossibleDataLoss(); }); this.tinymceEditor.on('keyup', e => { if(e.keyCode !== 13 && e.keyCode !== 40) { return; } let node = this.tinymceEditor.selection.getNode(); if( e.keyCode === 40 && node.tagName === 'PRE' && node.nextSibling === null ) { this.tinymceEditor.execCommand('mceInsertContent', false, '

    '); return; } if( e.keyCode === 13 && node.tagName === 'P' && node.getAttribute('class') ) { node.removeAttribute('class'); return; } if( e.keyCode === 13 && node.tagName === 'P' && node.parentNode.tagName === 'BLOCKQUOTE' && node.previousSibling && node.previousSibling.tagName === 'P' && node.previousSibling.childNodes && node.previousSibling.childNodes[0] && node.previousSibling.childNodes[0].tagName === 'BR' && node.previousSibling.childNodes[0].getAttribute('data-mce-bogus') === '1' && node.nextSibling === null ) { // get the element's parent node let parent = node.parentNode; if(parent.nextSibling) { parent.parentNode.insertBefore(node, parent.nextSibling); parent.removeChild(parent.lastChild); } else { parent.parentNode.appendChild(node); parent.removeChild(parent.lastChild); } setTimeout(() => { this.tinymceEditor.selection.select(parent.nextSibling, true); }, 0); } }); iframe.contentWindow.window.document.body.addEventListener("click", (e) => { let clickedElement = e.path ? e.path[0] : e.srcElement; let showPopup = false; if(localStorage.getItem('publii-writers-panel') === null) { localStorage.setItem('publii-writers-panel', 'opened'); window.app.writersPanelOpen(); } if(clickedElement.tagName === 'FIGCAPTION') { return; } if(clickedElement.tagName === 'SCRIPT') { let content = this.tinymceEditor.getContent({ source_view: true }); window.app.sourceCodeEditorShow(content, this.tinymceEditor); return; } if(clickedElement.tagName === 'FIGURE') { showPopup = true; } else if(e.path && e.path[1] && e.path[1].tagName === 'FIGURE') { clickedElement = e.path[1]; showPopup = true; } else if(e.srcElement && e.srcElement.parentNode && e.srcElement.parentNode === 'FIGURE') { clickedElement = e.srcElement.parentNode; showPopup = true; } if(clickedElement.tagName === 'A' || clickedElement.parentNode.tagName === 'A') { let selection = iframe.contentWindow.window.getSelection(); selection.removeAllRanges(); let range = iframe.contentWindow.window.document.createRange(); if (clickedElement.tagName === 'A') { range.selectNode(clickedElement); } else if (clickedElement.parentNode && clickedElement.parentNode.tagName === 'A') { range.selectNode(clickedElement.parentNode); } selection.addRange(range); if (this.checkInlineLinkTrigger(clickedElement)) { window.app.updateLinkEditor({ sel: selection, text: clickedElement }); } } else { window.app.updateLinkEditor({ sel: false, text: false }); } if( clickedElement.tagName === 'DIV' && clickedElement.getAttribute('class') && clickedElement.getAttribute('class').indexOf('gallery') !== -1 ) { window.app.updateGalleryPopup({ postID: this.itemID, galleryElement: clickedElement }); window.app.galleryPopupUpdated(this.galleryPopupUpdated.bind(this)); } }); let linkToolbar = $('#link-toolbar'); let inlineToolbar = $('#inline-toolbar'); let lastScroll = -1; let hideToolbars = function (e) { if (linkToolbar.css('display') !== 'block' && inlineToolbar.css('display') !== 'block') { return; } let iframeScrollOffset = iframe.contentWindow.document.body.parentNode.scrollTop; if (lastScroll !== -1 && Math.abs(iframeScrollOffset - lastScroll) > 20) { lastScroll = -1; linkToolbar.css('display', 'none'); inlineToolbar.css('display', 'none'); } else if (lastScroll === -1) { lastScroll = iframeScrollOffset; } }; iframe.contentWindow.window.addEventListener("scroll", hideToolbars); $('#post-editor-fake-image-uploader').on('change', () => { if (!$('#post-editor-fake-image-uploader')[0].value) { return; } setTimeout(async () => { if(this.callbackForTinyMCE) { let filePath = false; if($('#post-editor-fake-image-uploader')[0].files) { filePath = await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile($('#post-editor-fake-image-uploader')[0].files[0])); } if(!filePath) { return; } mainProcessAPI.send('app-image-upload', { id: this.itemID, site: window.app.getSiteName(), path: filePath, imageType: 'contentImages' }); mainProcessAPI.receiveOnce('app-image-uploaded', (data) => { let imagePath = data.baseImage.url; imagePath = imagePath.replace('file://', 'file:///'); this.callbackForTinyMCE(imagePath, { alt: '', dimensions: { height: data.baseImage.size[1], width: data.baseImage.size[0] } }); }); $('#post-editor-fake-image-uploader')[0].value = ''; } }, 50); }); // Writers Panel let updateWritersPanel = function () { window.app.writersPanelRefresh(); }; let throttledUpdate = Utils.debouncedFunction(updateWritersPanel, 1000); editor.on('setcontent beforeaddundo undo redo keyup', throttledUpdate); updateWritersPanel(); iframe.contentWindow.window.document.addEventListener('copy', () => { self.hideToolbarsOnCopyOrScroll(); }); iframe.contentWindow.window.document.addEventListener('scroll', () => { self.hideToolbarsOnCopyOrScroll(); }); // Clean up content before saving editor.on('GetContent', function (e) { if (e.format === 'html') { // Remove contenteditable from output e.content = e.content.replace(/\s*contenteditable="(true|false)"/gi, ''); // Remove empty paragraphs after figures e.content = e.content.replace(/<\/figure>\s*

    \s*( |\u00a0)?\s*<\/p>/gi, '

    '); // Clean up double figures e.content = e.content.replace(/]*>\s*]*>/gi, '
    '); e.content = e.content.replace(/<\/figure>\s*<\/figure>/gi, '
    '); } }); }); editor.ui.registry.addButton('gallery', { icon: 'gallery', tooltip: window.app.translate('editor.insertGallery'), onAction: function () { editor.insertContent(''); } }); } extensionsPath () { return [ 'file:///', window.app.getSiteDir(), '/input/themes/', window.app.getSiteTheme(), '/' ].join(''); } galleryPopupUpdated (response) { this.hideToolbarsOnCopyOrScroll(); if(response) { response.gallery.innerHTML = response.html; response.gallery.setAttribute('data-is-empty', response.html === ' '); } } getTinyMCECSSFiles () { let pathToEditorCSS = this.extensionsPath() + 'assets/css/editor.css'; let customEditorCSS = pathToEditorCSS; return [ 'css/editor.css?v=0710', customEditorCSS ].join(','); } getCustomThemeEditorConfig () { // Add custom editor config let customEditorConfig = false; if (window.app.hasPostEditorConfigOverride()) { let configOverridePath = this.extensionsPath() + 'tinymce.override.json'; jQuery.ajax({ url: configOverridePath, dataType: 'json', async: false, success: function(json) { customEditorConfig = json; } }); } return customEditorConfig; } loadCustomFormatsFromTheme() { let output = []; let customElements = []; let inlineElements = [ 'a', 'b', 'abbr', 'acronym', 'cite', 'dfn', 'kbd', 'samp', 'time', 'var', 'bdo', 'br', 'big', 'code', 'i', 'em', 'small','strong','span', 'tt', 'img', 'map', 'object', 'q', 'script', 'sub', 'sup', 'button', 'input', 'label', 'select', 'textarea' ]; // Detect mode if (window.app.getThemeCustomElementsMode() === 'advanced') { output = JSON.parse(JSON.stringify(window.app.getThemeCustomElements())); return output; } // Load custom elements if (window.app.getThemeCustomElements()) { customElements = window.app.getThemeCustomElements(); } if(customElements && customElements.length) { for(let i = 0; i < customElements.length; i++) { if(!customElements[i]) { continue; } if(!customElements[i].tag && !customElements[i].selector) { continue; } if(customElements[i].postEditor === false) { continue; } let style = { title: customElements[i].label, classes: customElements[i].cssClasses }; if(customElements[i].selector) { style.selector = customElements[i].selector; } else { if (inlineElements.indexOf(customElements[i].tag)) { style.inline = customElements[i].tag; } else { style.block = customElements[i].tag; } } output.push(style); } } return output; } filePickerCallback(callback, value, meta) { // Provide image and alt text for the image dialog if (meta.filetype == 'image') { this.callbackForTinyMCE = callback; $('#post-editor-fake-image-uploader').trigger('click'); } else { this.callbackForTinyMCE = false; } } addEditorButtons() { this.tinymceEditor.ui.registry.addButton("publiilink", { icon: 'link', tooltip: window.app.translate('link.insertEditLink'), onAction: () => { let selectedNode = tinymce.activeEditor.selection.getNode(); if (selectedNode.tagName === 'IMG' && selectedNode.parentNode && selectedNode.parentNode.tagName === 'A') { window.app.initLinkPopup({ postID: this.itemID, selection: selectedNode.parentNode.outerHTML }); } else { window.app.initLinkPopup({ postID: this.itemID, selection: tinymce.activeEditor.selection.getContent() }); } } }); this.tinymceEditor.ui.registry.addButton("sourcecode", { icon: 'sourcecode', tooltip: window.app.translate('editor.sourceCode'), text: "HTML", onAction: () => { let content = this.tinymceEditor.getContent({ source_view: true }); window.app.sourceCodeEditorShow(content, this.tinymceEditor); } }); this.tinymceEditor.ui.registry.addButton('readmore', { icon: 'readmore', text: window.app.translate('editor.readMore'), onAction: () => { this.tinymceEditor.insertContent('
    ' + "\n"); } }); } addInlineEditor(customFormats) { let iframe = document.getElementById('post-editor_ifr'); let win = iframe.contentWindow.window; let doc = win.document; window.app.initInlineEditor('init-inline-editor', customFormats); $(doc.querySelector('html')).on('mouseup', (e) => { let sel = win.getSelection(); let text = sel.toString(); if (this.checkInlineTrigger(e.target)) { window.app.updateInlineEditor({ sel, text }); } }); } checkInlineTrigger (target) { let excludedTags = ['FIGURE', 'FIGCAPTION', 'IMG', 'PRE']; if (excludedTags.indexOf(target.tagName) > -1) { return false; } if (target.tagName === 'DIV' && target.classList.contains('gallery')) { return false; } for ( ; target && target !== document; target = target.parentNode) { if (target.matches && target.matches('.post__toc')) { return false; } if (target.matches && target.matches('pre')) { return false; } } return true; } checkInlineLinkTrigger (target) { for ( ; target && target !== document; target = target.parentNode) { if (target.matches && target.matches('.post__toc')) { return false; } } return true; } addLinkEditor(iframe) { window.app.initLinkEditor(iframe); } hideToolbarsOnCopyOrScroll() { $('#link-toolbar').css('display', 'none'); $('#inline-toolbar').css('display', 'none'); } initEditorDragNDropImages(editor) { let editorArea = $('.tox-tinymce'); let postEditor = $('.post-editor'); let hoverState = false; let tinymceOverlay = $('.tinymce-overlay'); postEditor.on('dragover', () => { if(!this.postEditorInnerDragging && !$('.popup.gallery-popup').length) { hoverState = true; editorArea.addClass('is-hovered'); } }); tinymceOverlay.on('dragover', e => { if(!this.postEditorInnerDragging && !$('.popup.gallery-popup').length) { hoverState = true; editorArea.addClass('is-hovered'); } }); postEditor.on('dragleave', () => { hoverState = false; setTimeout(() => { if(!hoverState) { editorArea.removeClass('is-hovered'); } }, 250); }); document.getElementById('post-editor_ifr').contentWindow.addEventListener("dragover", e => { if(!this.postEditorInnerDragging) { e.preventDefault(); e.stopPropagation(); editorArea.addClass('is-hovered'); } }, false); document.getElementById('post-editor_ifr').contentWindow.addEventListener('mousedown', () => { this.postEditorInnerDragging = true; }); document.getElementById('post-editor_ifr').contentWindow.addEventListener('mouseup', () => { this.postEditorInnerDragging = false; }); document.getElementById('post-editor_ifr').contentWindow.addEventListener('mouseout', () => { this.postEditorInnerDragging = false; }); editorArea.on('dragover', this.fileDragOver.bind(this)); editorArea.on('drop', this.editorFileSelect.bind(this)); } fileDragOver (e) { if(!this.postEditorInnerDragging) { e.originalEvent.stopPropagation(); e.originalEvent.preventDefault(); e.originalEvent.dataTransfer.dropEffect = 'copy'; } } async editorFileSelect (e) { e.originalEvent.stopPropagation(); e.originalEvent.preventDefault(); let files = e.originalEvent.dataTransfer.files; let siteName = window.app.getSiteName(); if(this.postEditorInnerDragging) { return; } if(!files[0]) { $('.tox-tinymce').removeClass('is-hovered'); $('.tox-tinymce').removeClass('is-loading-image'); $('.tinymce-overlay').text('Drag your image here'); this.contentImageUploading = false; return; } $('.tox-tinymce').addClass('is-loading-image'); $('.tinymce-overlay').html('
    ' + 'Upload in progress
    '); mainProcessAPI.send('app-image-upload', { "id": this.itemID, "site": siteName, "path": await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(files[0])) }); this.contentImageUploading = true; mainProcessAPI.receiveOnce('app-image-uploaded', (data) => { if(data.baseImage && data.baseImage.size && data.baseImage.size[0] && data.baseImage.size[1]) { tinymce.activeEditor.insertContent('

    '); } else { tinymce.activeEditor.insertContent('

    '); } $('.tox-tinymce').removeClass('is-hovered'); $('.tox-tinymce').removeClass('is-loading-image'); $('.tinymce-overlay').html('
    Drag image here
    '); this.contentImageUploading = false; }); } reloadEditor () { this.tinymceEditor.once('keyup', e => { window.app.reportPossibleDataLoss(); }); } } export default EditorBridge; ================================================ FILE: app/src/components/post-editor/GalleryPopup.vue ================================================ ================================================ FILE: app/src/components/post-editor/HelpPanelBlockEditor.vue ================================================ ================================================ FILE: app/src/components/post-editor/HelpPanelMarkdown.vue ================================================ ================================================ FILE: app/src/components/post-editor/InlineEditor.vue ================================================ ================================================ FILE: app/src/components/post-editor/ItemHelper.js ================================================ class ItemHelper { static async prepareItemData (newStatus, itemID, $store, itemData, itemType = 'post') { let finalStatus = newStatus; let mediaPath = ItemHelper.getMediaPath($store, itemID, itemType); let preparedText = ''; if (itemData.editor === 'tinymce') { preparedText = $('#post-editor').val(); } if (itemData.editor === 'markdown') { preparedText = itemData.text; } if (itemData.editor === 'blockeditor') { preparedText = $('#post-editor').val(); } // Remove directxory path from images src attribute preparedText = preparedText.replace(/file:(\/){1,}/gmi, 'file:///'); preparedText = preparedText.split(mediaPath).join('#DOMAIN_NAME#'); preparedText = preparedText.replace(/file:(\/){1,}\#DOMAIN_NAME\#/gmi, '#DOMAIN_NAME#'); if (itemType === 'page') { finalStatus += ',is-page'; } if (itemData.isHidden) { finalStatus += ',hidden'; } if (itemData.isTrashed) { finalStatus += ',trashed'; } if (itemData.isFeatured) { finalStatus += ',featured'; } if (itemData.isExcludedOnHomepage) { finalStatus += ',excluded_homepage'; } let itemViewSettings = {}; if (itemType === 'post') { $store.state.currentSite.themeSettings.postConfig.forEach(field => { let fieldType = 'select'; if (typeof field.type !== 'undefined') { fieldType = field.type; } itemViewSettings[field.name] = { type: fieldType, value: itemData.viewOptions[field.name] }; }); } else if (itemType === 'page') { $store.state.currentSite.themeSettings.pageConfig.forEach(field => { let fieldType = 'select'; if (typeof field.type !== 'undefined') { fieldType = field.type; } itemViewSettings[field.name] = { type: fieldType, value: itemData.viewOptions[field.name] }; }); } if (itemData.slug === '') { itemData.slug = await mainProcessAPI.invoke('app-main-process-create-slug', itemData.title); } let creationDate = itemData.creationDate.timestamp; if (!itemData.creationDate.timestamp) { creationDate = Date.now(); } let preparedData = { 'site': $store.state.currentSite.config.name, 'title': itemData.title, 'slug': itemData.slug, 'text': preparedText, 'status': finalStatus, 'creationDate': creationDate, 'modificationDate': Date.now(), 'template': itemData.template, 'featuredImage': itemData.featuredImage.path === null ? '' : 'file:///' + ItemHelper.getMediaPath($store, itemID, itemType) + itemData.featuredImage.path, 'featuredImageFilename': itemData.featuredImage.path, 'featuredImageData': { alt: itemData.featuredImage.alt, caption: itemData.featuredImage.caption, credits: itemData.featuredImage.credits }, 'additionalData': { metaTitle: itemData.metaTitle, metaDesc: itemData.metaDescription, metaRobots: itemData.metaRobots, canonicalUrl: itemData.canonicalUrl, editor: itemData.editor }, 'id': itemID, 'author': parseInt(itemData.author, 10) }; if (itemType === 'post') { preparedData.tags = itemData.tags; if (preparedData.tags && preparedData.tags.length) { preparedData.tags = [...new Set(preparedData.tags)]; } preparedData.additionalData.mainTag = itemData.mainTag; preparedData.postViewSettings = itemViewSettings; } else if (itemType === 'page') { preparedData.pageViewSettings = itemViewSettings; } return preparedData; } static loadItemData (data, $store, $moment, itemType = 'post') { let itemData = { title: '', text: '', slug: '', author: 1, template: '', creationDate: { text: '', timestamp: 0 }, modificationDate: { text: '', timestamp: 0 }, isTrashed: false, status: '', metaTitle: '', metaDescription: '', metaRobots: 'index, follow', canonicalUrl: '', featuredImage: { path: '', alt: '', caption: '', credits: '' } }; if (itemType === 'post') { itemData.tags = []; itemData.viewOptions = {}; itemData.mainTag = ''; itemData.isFeatured = false; itemData.isHidden = false; itemData.isExcludedOnHomepage = false; } else if (itemType === 'page') { itemData.viewOptions = {}; } // Set post elements itemData.title = data[itemType + 's'][0].title; let mediaPath = ItemHelper.getMediaPath($store, data[itemType + 's'][0].id, itemType); let preparedText = data[itemType + 's'][0].text; preparedText = preparedText.split('#DOMAIN_NAME#').join('file:///' + mediaPath); preparedText = ItemHelper.setWebpCompatibility($store, preparedText); itemData.text = preparedText; // Set tags if (itemType === 'post') { itemData.tags = []; if (data.tags.length) { for (let i = 0; i < data.tags.length; i++) { itemData.tags.push(data.tags[i].name); } } itemData.mainTag = data.additionalData.mainTag || ''; } // Set author itemData.author = data.author[0].id; // Dates let format = 'MMM DD, YYYY HH:mm'; if($store.state.app.config.timeFormat == 12) { format = 'MMM DD, YYYY hh:mm a'; } itemData.creationDate.text = $moment(data[itemType + 's'][0].created_at).format(format); itemData.modificationDate.text = $moment(data[itemType + 's'][0].modified_at).format(format); itemData.creationDate.timestamp = data[itemType + 's'][0].created_at; itemData.modificationDate.timestamp = data[itemType + 's'][0].modified_at; itemData.status = data[itemType + 's'][0].status.split(',').join(', '); itemData.isTrashed = data[itemType + 's'][0].status.indexOf('trashed') > -1; if (itemType === 'post') { itemData.isHidden = data[itemType + 's'][0].status.indexOf('hidden') > -1; itemData.isFeatured = data[itemType + 's'][0].status.indexOf('featured') > -1; itemData.isExcludedOnHomepage = data[itemType + 's'][0].status.indexOf('excluded_homepage') > -1; } // Set image if (data.featuredImage) { itemData.featuredImage.path = !data.featuredImage.url ? '' : data.featuredImage.url; if(data.featuredImage.additional_data) { try { let imageData = JSON.parse(data.featuredImage.additional_data); itemData.featuredImage.alt = imageData.alt; itemData.featuredImage.caption = imageData.caption; itemData.featuredImage.credits = imageData.credits; } catch(e) { console.warning('Unable to load featured image data: '); console.warning(data.featuredImage.additional_data); } } } // Set SEO itemData.slug = data[itemType + 's'][0].slug; itemData.metaTitle = data.additionalData.metaTitle || ""; itemData.metaDescription = data.additionalData.metaDesc || ""; itemData.metaRobots = data.additionalData.metaRobots || ""; itemData.canonicalUrl = data.additionalData.canonicalUrl || ""; // Update post template itemData.template = data[itemType + 's'][0].template; // Update item view settings if (itemType === 'post' || itemType === 'page') { ItemHelper.updateViewSettings(itemType, data, itemData); } return itemData; } static updateViewSettings(itemType, data, itemData) { let settingsKey = itemType + 'ViewSettings'; let viewSettings = data[settingsKey]; let viewFields = Object.keys(viewSettings); for (let i = 0; i < viewFields.length; i++) { let newValue = ''; if (viewSettings[viewFields[i]] && viewSettings[viewFields[i]].value) { newValue = viewSettings[viewFields[i]].value; } else { newValue = viewSettings[viewFields[i]]; } itemData.viewOptions[viewFields[i]] = newValue; } } static getMediaPath ($store, itemID, itemType = 'post') { let mediaPath = $store.state.currentSite.siteDir.replace(/&/gmi, '&'); mediaPath = mediaPath.replace(/\\/g, '/'); mediaPath += '/input/media/'; if (itemType === 'post' || itemType === 'page') { mediaPath += 'posts/'; } mediaPath += itemID === 0 ? 'temp' : itemID; mediaPath += '/'; return mediaPath; } static setWebpCompatibility ($store, text) { let forceWebp = !!$store.state.currentSite.config.advanced.forceWebp; text = text.replace(/\