diff --git a/.nvmrc b/.nvmrc index eb800ed45..1efe0ac63 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.19.0 +v20.15.1 diff --git a/README.md b/README.md index b3f5a0bb8..24f0726e4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [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.45.2 (build 16609)** +**Current version: 0.46.0 (build 16889)** ## Why Publii? Unlike static-site generators that are often unwieldy and difficult to use, Publii provides an @@ -82,4 +82,4 @@ Support this project by becoming a sponsor. Your logo will show up here with a l ## License -Copyright (c) 2022 TidyCustoms. General Public License v3.0, read [LICENSE](https://getpublii.com/license.html) for details. +Copyright (c) 2024 TidyCustoms. General Public License v3.0, read [LICENSE](https://getpublii.com/license.html) for details. diff --git a/app/back-end/app-preload.js b/app/back-end/app-preload.js index c7a34cf90..2f4f6481e 100644 --- a/app/back-end/app-preload.js +++ b/app/back-end/app-preload.js @@ -50,11 +50,19 @@ contextBridge.exposeInMainWorld('mainProcessAPI', { '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', @@ -67,6 +75,7 @@ contextBridge.exposeInMainWorld('mainProcessAPI', { 'app-license-accept', 'app-deploy-render-abort', 'app-deploy-abort', + 'app-deploy-continue', 'app-deploy-render', 'app-deploy-upload', 'app-sync-is-done', @@ -115,7 +124,8 @@ contextBridge.exposeInMainWorld('mainProcessAPI', { 'app-wxr-import-progress', 'app-show-search-form', 'block-editor-undo', - 'block-editor-redo' + 'block-editor-redo', + 'no-remote-files' ]; if (validChannels.includes(channel)) { @@ -158,6 +168,12 @@ contextBridge.exposeInMainWorld('mainProcessAPI', { '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', diff --git a/app/back-end/app.js b/app/back-end/app.js index a4b4c95bb..69839d5e0 100644 --- a/app/back-end/app.js +++ b/app/back-end/app.js @@ -14,6 +14,7 @@ const url = require('url'); 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'); @@ -224,9 +225,11 @@ class App { 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 = fs.readFileSync(menuConfigPath, 'utf8'); let parsedMenuStructure = {}; @@ -240,14 +243,18 @@ class App { 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 diff --git a/app/back-end/author.js b/app/back-end/author.js index ed042d444..b6aa7363a 100644 --- a/app/back-end/author.js +++ b/app/back-end/author.js @@ -2,6 +2,7 @@ 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'); @@ -23,6 +24,7 @@ class Author extends Model { 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) { @@ -133,6 +135,7 @@ class Author extends Model { message: 'author-added', authorID: this.id, postsAuthors: this.postsData.loadAuthorsXRef(), + pagesAuthors: this.pagesData.loadAuthorsXRef(), authors: this.authorsData.load() }; } @@ -292,7 +295,7 @@ class Author extends Model { featuredImage = path.parse(this.additionalData.featuredImage).base; } - // If post is cancelled - get the previous featured image + // If author is cancelled - get the previous featured image if (cancelEvent && this.id !== 0) { let featuredImageSqlQuery = `SELECT additional_data FROM authors WHERE id = @id`; @@ -313,6 +316,12 @@ class Author extends Model { authorDir = 'temp'; } + let imagesInAuthorViewSettings = []; + + if (this.additionalData && this.additionalData.viewConfig) { + imagesInAuthorViewSettings = Object.values(this.additionalData.viewConfig).filter(item => item.type === "image").map(item => item.value); + } + // Iterate through images for (let i in images) { let imagePath = images[i]; @@ -323,7 +332,14 @@ class Author extends Model { continue; } - if ((cancelEvent && authorDir === 'temp') || featuredImage !== imagePath) { + // Remove files which does not exist as featured image and authorViewSettings + if( + (cancelEvent && authorDir === 'temp') || + ( + imagesInAuthorViewSettings.indexOf(imagePath) === -1 && + featuredImage !== imagePath + ) + ) { try { fs.unlinkSync(fullPath); } catch(e) { @@ -337,8 +353,11 @@ class Author extends Model { // 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'); - let themeConfigString = fs.readFileSync(themeConfigPath, 'utf8'); - themesHelper.checkAndCleanImages(themeConfigString); + + if (fs.fileExists(themeConfigPath)) { + let themeConfigString = fs.readFileSync(themeConfigPath, 'utf8'); + themesHelper.checkAndCleanImages(themeConfigString); + } } /* diff --git a/app/back-end/builddata.json b/app/back-end/builddata.json index ff0e326d9..a493ca240 100644 --- a/app/back-end/builddata.json +++ b/app/back-end/builddata.json @@ -1,4 +1,4 @@ { - "version": "0.45.2", - "build": 16609 + "version": "0.46.0", + "build": 16889 } diff --git a/app/back-end/events/_modules.js b/app/back-end/events/_modules.js index 43af97f19..2f98ffe09 100644 --- a/app/back-end/events/_modules.js +++ b/app/back-end/events/_modules.js @@ -5,6 +5,7 @@ module.exports = { AppEvents: require('./app.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'), diff --git a/app/back-end/events/deploy.js b/app/back-end/events/deploy.js index 79d1c58d0..3c1230b4e 100644 --- a/app/back-end/events/deploy.js +++ b/app/back-end/events/deploy.js @@ -65,6 +65,21 @@ class DeployEvents { 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); @@ -162,7 +177,7 @@ class DeployEvents { }); this.deploymentProcess.on('message', function(data) { - if(data.type === 'web-contents') { + if (data.type === 'web-contents') { if(data.value) { self.app.mainWindow.webContents.send(data.message, data.value); } else { diff --git a/app/back-end/events/page.js b/app/back-end/events/page.js new file mode 100644 index 000000000..a6c4e7f46 --- /dev/null +++ b/app/back-end/events/page.js @@ -0,0 +1,102 @@ +const fs = require('fs'); +const path = require('path'); +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)) { + event.sender.send('app-pages-hierarchy-loaded', JSON.parse(fs.readFileSync(pagesFile, { encoding: 'utf8' }))); + } 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'); + fs.writeFileSync(pagesFile, JSON.stringify(pagesData.hierarchy, null, 4), { encoding: 'utf8' }); + }); + } +} + +module.exports = PageEvents; diff --git a/app/back-end/events/preview.js b/app/back-end/events/preview.js index eaca5f725..d718026b9 100644 --- a/app/back-end/events/preview.js +++ b/app/back-end/events/preview.js @@ -88,6 +88,10 @@ class PreviewEvents { errorDesc = data.result[0].message + "\n\n" + data.result[0].desc; } + if (typeof errorDesc === 'object') { + errorDesc = errorDesc.translation; + } + event.sender.send('app-preview-render-error', { message: [{ message: errorTitle, @@ -170,7 +174,7 @@ class PreviewEvents { url = path.join(basePath, 'index.html'); - if (mode === 'tag' || mode === 'post' || mode === 'author') { + if (mode === 'tag' || mode === 'post' || mode === 'page' || mode === 'author') { url = path.join(basePath, 'preview.html'); } diff --git a/app/back-end/events/site.js b/app/back-end/events/site.js index e89af7fd6..514e727ce 100644 --- a/app/back-end/events/site.js +++ b/app/back-end/events/site.js @@ -54,6 +54,7 @@ class SiteEvents { // 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); @@ -392,6 +393,7 @@ class SiteEvents { newConfig: { config: themeConfig.config, customConfig: themeConfig.customConfig, + pageConfig: themeConfig.pageConfig, postConfig: themeConfig.postConfig, tagConfig: themeConfig.tagConfig, authorConfig: themeConfig.authorConfig, diff --git a/app/back-end/image.js b/app/back-end/image.js index 6a8032e40..7dc3490ca 100644 --- a/app/back-end/image.js +++ b/app/back-end/image.js @@ -23,6 +23,8 @@ class Image extends Model { if (imageData.id === 'website') { this.id = 'website'; + } else if (imageData.id === 'defaults') { + this.id = 'defaults'; } // App instance @@ -95,9 +97,18 @@ class Image extends Model { let galleryDirPath = ''; let responsiveDirPath = ''; - if (this.imageType === 'pluginImages') { + 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') { + } 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) { diff --git a/app/back-end/modules/deploy/deployment.js b/app/back-end/modules/deploy/deployment.js index 4d858a1b2..f4e83b3b7 100644 --- a/app/back-end/modules/deploy/deployment.js +++ b/app/back-end/modules/deploy/deployment.js @@ -23,8 +23,8 @@ const ManualDeployment = require('./manual.js'); * (S)FTP(S), * S3 server, * Git - * Github Pages (deprecated), - * Gitlab Pages (deprecated), + * Github Pages, + * Gitlab Pages, * Netlify, * Google Cloud, * Manually @@ -38,7 +38,7 @@ class Deployment { * @param sitesDir * @param siteConfig */ - constructor(appDir, sitesDir, siteConfig, useAltFtp) { + constructor (appDir, sitesDir, siteConfig, useAltFtp) { this.appDir = appDir; this.siteConfig = siteConfig; this.siteName = this.siteConfig.name; @@ -60,7 +60,7 @@ class Deployment { * @param deploymentConfig * @param siteName */ - async testConnection(app, deploymentConfig, siteName, uuid) { + async testConnection (app, deploymentConfig, siteName, uuid) { let connection = false; switch(deploymentConfig.protocol) { @@ -89,7 +89,7 @@ class Deployment { /** * Inits connection */ - async initSession() { + async initSession () { switch(this.siteConfig.deployment.protocol) { case 'sftp': case 'sftp+key': this.client = new SFTP(this); break; @@ -115,7 +115,7 @@ class Deployment { /** * Set input directory on local machine */ - setInput() { + 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'); @@ -125,8 +125,8 @@ class Deployment { /** * Sets output directory on the server */ - setOutput(useEmpty = false) { - if(useEmpty) { + setOutput (useEmpty = false) { + if (useEmpty) { this.outputDir = ''; } else { this.outputDir = this.siteConfig.deployment.path; @@ -218,70 +218,48 @@ class Deployment { fileContent = fileContent.toString(); } - let syncRevisionPath = path.join(this.configDir, 'sync-revision.json'); let content = JSON.parse(fileContent); - let revisionID = false; - - if (fs.existsSync(syncRevisionPath)) { - let syncRevisionContent = fs.readFileSync(syncRevisionPath); - syncRevisionContent = JSON.parse(syncRevisionContent); - revisionID = syncRevisionContent.revision; - } if (content.revision) { - let filesToCheck = fs.readFileSync(path.join(this.configDir, 'files-remote.json')); + let syncRevisionPath = path.join(this.configDir, 'sync-revision.json'); + let revisionID = false; + + if (fs.existsSync(syncRevisionPath)) { + let syncRevisionContent = fs.readFileSync(syncRevisionPath); + syncRevisionContent = JSON.parse(syncRevisionContent); + revisionID = syncRevisionContent.revision; + } if (revisionID) { let isExpectedCopy = revisionID === content.revision; this.compareFilesList(isExpectedCopy); } else { + let filesToCheck = fs.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); } - - return; + } else { + fs.writeFileSync(path.join(this.configDir, 'files-remote.json'), fileContent); + this.compareFilesList(true); } - - // when files on server uses old format - download it and use as remote files list - fs.writeFileSync(path.join(this.configDir, 'files-remote.json'), fileContent); - this.compareFilesList(true); } catch (e) { this.compareFilesList(false); } } - /** - * Save files.publii.json as files-remote.json and store checksum in the file - */ - replaceSyncInfoFiles () { - let inputListPath = path.join(this.inputDir, 'files.publii.json'); - let remoteListPath = path.join(this.configDir, 'files-remote.json'); - let syncRevisionPath = path.join(this.configDir, 'sync-revision.json'); - let contentToSave = fs.readFileSync(inputListPath); - let newContent = `{ "revision": "${this.syncRevision}" }`; - fs.writeFileSync(remoteListPath, contentToSave); - fs.writeFileSync(inputListPath, newContent); - fs.writeFileSync(syncRevisionPath, newContent); - } - /** * Compares remote and local files lists * * @param remoteFileListExists */ - compareFilesList(remoteFileListExists = false) { - let localFiles = fs.readFileSync(path.join(this.inputDir, 'files.publii.json'), 'utf8'); + compareFilesList (remoteFileListExists = false) { let remoteFiles = false; - if(localFiles) { - localFiles = JSON.parse(localFiles); - } - - if(remoteFileListExists) { + if (remoteFileListExists) { remoteFiles = fs.readFileSync(path.join(this.configDir, 'files-remote.json'), 'utf8'); - if(remoteFiles) { + if (remoteFiles) { try { remoteFiles = JSON.parse(remoteFiles); @@ -297,24 +275,43 @@ class Deployment { } } - if(!remoteFiles) { - remoteFiles = []; + // 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 = fs.readFileSync(path.join(this.inputDir, 'files.publii.json'), 'utf8'); + + if (localFiles) { + localFiles = JSON.parse(localFiles); } // Detect files to remove let filesToRemove = []; - for(let remoteFile of remoteFiles) { + for (let remoteFile of remoteFiles) { let fileFounded = false; - for(let localFile of localFiles) { - if(localFile.path === remoteFile.path) { + for (let localFile of localFiles) { + if (localFile.path === remoteFile.path) { fileFounded = true; break; } } - if(!fileFounded) { + if (!fileFounded) { if ( (this.siteConfig.deployment.protocol === 'google-cloud' || this.siteConfig.deployment.protocol === 'gitlab-pages') && remoteFile.type === 'directory' @@ -332,10 +329,10 @@ class Deployment { // Detect files to upload let filesToUpload = []; - for(let localFile of localFiles) { + for (let localFile of localFiles) { let fileShouldBeUploaded = true; - for(let remoteFile of remoteFiles) { + for (let remoteFile of remoteFiles) { if( localFile.path === remoteFile.path && localFile.md5 === remoteFile.md5 @@ -347,7 +344,10 @@ class Deployment { if (fileShouldBeUploaded) { if ( - (this.siteConfig.deployment.protocol === 'google-cloud' || this.siteConfig.deployment.protocol === 'gitlab-pages') && + ( + this.siteConfig.deployment.protocol === 'google-cloud' || + this.siteConfig.deployment.protocol === 'gitlab-pages' + ) && localFile.type === 'directory' ) { continue; @@ -363,7 +363,7 @@ class Deployment { this.filesToRemove = filesToRemove; this.filesToUpload = filesToUpload; - if(this.siteConfig.deployment.protocol === 's3') { + 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 { @@ -390,9 +390,7 @@ class Deployment { /** * Move files or directories to the beginning */ - sortFiles() { - let self = this; - + sortFiles () { this.filesToRemove = this.filesToRemove.sort(function(fileA, fileB) { if(fileA.type === 'directory') { return -1; @@ -428,19 +426,19 @@ class Deployment { // 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) { + if (fileA.type === 'directory' && fileB.type === 'directory') { + if (fileA.path.length <= fileB.path.length) { return 1; } else { return -1; } } - if(fileA.type === 'directory') { + if (fileA.type === 'directory') { return 1; } - if(fileB.type === 'directory') { + if (fileB.type === 'directory') { return -1; } @@ -455,23 +453,23 @@ class Deployment { /** * Removes file */ - removeFile() { - if(this.siteConfig.deployment.protocol === 's3') { + removeFile () { + if (this.siteConfig.deployment.protocol === 's3') { this.client.removeFile(); return; } - if(this.siteConfig.deployment.protocol === 'gitlab-pages') { + if (this.siteConfig.deployment.protocol === 'gitlab-pages') { this.client.startSync(); return; } let self = this; - if(this.filesToRemove.length > 0) { + if (this.filesToRemove.length > 0) { let fileToRemove = this.filesToRemove.pop(); - if(fileToRemove.type === 'file') { + 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))); @@ -495,13 +493,13 @@ class Deployment { /** * Uploads file */ - uploadFile() { + uploadFile () { let self = this; - if(this.filesToUpload.length > 0) { + if (this.filesToUpload.length > 0) { let fileToUpload = this.filesToUpload.pop(); - if(fileToUpload.type === 'file') { + if (fileToUpload.type === 'file') { this.client.uploadFile( normalizePath(path.join(this.inputDir, fileToUpload.path)), normalizePath(path.join(this.outputDir, fileToUpload.path)) @@ -518,7 +516,10 @@ class Deployment { message: 'app-uploading-progress', value: { progress: 98, - operations: [self.currentOperationNumber ,self.operationsCounter] + operations: [ + self.currentOperationNumber, + self.operationsCounter + ] } }); @@ -535,7 +536,7 @@ class Deployment { * * @returns {Array} */ - readDirRecursiveSync(dir, filelist) { + readDirRecursiveSync (dir, filelist) { let self = this; let files = fs.readdirSync(dir); filelist = filelist || []; @@ -564,7 +565,7 @@ class Deployment { * @param files * @param suffix */ - saveConnectionFilesLog(files, suffix = '') { + saveConnectionFilesLog (files, suffix = '') { if (suffix !== '') { suffix = '-' + suffix; } diff --git a/app/back-end/modules/deploy/ftp-alt.js b/app/back-end/modules/deploy/ftp-alt.js index 9a87006c1..7c72df9fb 100644 --- a/app/back-end/modules/deploy/ftp-alt.js +++ b/app/back-end/modules/deploy/ftp-alt.js @@ -143,8 +143,6 @@ class FTPAlt { } }); - this.deployment.replaceSyncInfoFiles(); - try { await this.connection.uploadFrom( normalizePath(path.join(this.deployment.inputDir, 'files.publii.json')), diff --git a/app/back-end/modules/deploy/ftp.js b/app/back-end/modules/deploy/ftp.js index 65823a61d..40a6cd502 100644 --- a/app/back-end/modules/deploy/ftp.js +++ b/app/back-end/modules/deploy/ftp.js @@ -201,8 +201,6 @@ class FTP { } }); - this.deployment.replaceSyncInfoFiles(); - this.connection.put( normalizePath(path.join(this.deployment.inputDir, 'files.publii.json')), normalizePath(path.join(this.deployment.outputDir, 'files.publii.json')), diff --git a/app/back-end/modules/deploy/gitlab-pages.js b/app/back-end/modules/deploy/gitlab-pages.js index cd0d4332a..c0ba0e719 100644 --- a/app/back-end/modules/deploy/gitlab-pages.js +++ b/app/back-end/modules/deploy/gitlab-pages.js @@ -429,8 +429,6 @@ class GitlabPages { updateFilesListFile () { this.setUploadProgress(98); - this.deployment.replaceSyncInfoFiles(); - let localFilesListPath = path.join(this.deployment.inputDir, 'files.publii.json'); let localFilesContent = fs.readFileSync(localFilesListPath); let actionType = 'create'; diff --git a/app/back-end/modules/deploy/google-cloud.js b/app/back-end/modules/deploy/google-cloud.js index fa910a858..fe7a79dc7 100644 --- a/app/back-end/modules/deploy/google-cloud.js +++ b/app/back-end/modules/deploy/google-cloud.js @@ -125,8 +125,6 @@ class GoogleCloud { } }); - self.deployment.replaceSyncInfoFiles(); - this.connection.upload(fileToUpload, { destination: fileDestination }, function(err) { diff --git a/app/back-end/modules/deploy/manual.js b/app/back-end/modules/deploy/manual.js index 2ac190460..855d0f1f2 100644 --- a/app/back-end/modules/deploy/manual.js +++ b/app/back-end/modules/deploy/manual.js @@ -16,8 +16,7 @@ class ManualDeployment { async initConnection() { this.deployment.setInput(); this.deployment.prepareLocalFilesList(); - this.deployment.replaceSyncInfoFiles(); - + switch(this.deployment.siteConfig.deployment.manual.output) { case 'catalog': this.returnCatalog(); break; case 'zip-archive': this.returnZipArchive(); break; diff --git a/app/back-end/modules/deploy/netlify.js b/app/back-end/modules/deploy/netlify.js index 6cc949ad8..f62c4f521 100644 --- a/app/back-end/modules/deploy/netlify.js +++ b/app/back-end/modules/deploy/netlify.js @@ -40,8 +40,8 @@ class Netlify { localDir = this.deployment.inputDir; client = new NetlifyAPI({ - accessToken: token, - siteID: siteID, + accessToken: (token).toString().trim(), + siteID: (siteID).toString().trim(), inputDir: localDir }, { onStart: this.onStart.bind(this), @@ -161,8 +161,8 @@ class Netlify { } client = new NetlifyAPI({ - accessToken: token, - siteID: siteID, + accessToken: (token).toString().trim(), + siteID: (siteID).toString().trim(), inputDir: '' }); diff --git a/app/back-end/modules/deploy/s3.js b/app/back-end/modules/deploy/s3.js index aff9b8444..92d784e58 100644 --- a/app/back-end/modules/deploy/s3.js +++ b/app/back-end/modules/deploy/s3.js @@ -155,7 +155,6 @@ class S3 { async uploadNewFileList() { this.sendProgress(99); - this.deployment.replaceSyncInfoFiles(); let fileName = 'files.publii.json'; if (typeof this.prefix === 'string' && this.prefix !== '') { diff --git a/app/back-end/modules/deploy/sftp.js b/app/back-end/modules/deploy/sftp.js index b01f07cd3..a5906ab4c 100644 --- a/app/back-end/modules/deploy/sftp.js +++ b/app/back-end/modules/deploy/sftp.js @@ -157,8 +157,6 @@ class SFTP { } }); - self.deployment.replaceSyncInfoFiles(); - this.connection.put( normalizePath(path.join(self.deployment.inputDir, 'files.publii.json')), normalizePath(path.join(self.deployment.outputDir, 'files.publii.json')), diff --git a/app/back-end/modules/import/import.js b/app/back-end/modules/import/import.js index 76feb115c..c6951bdb6 100644 --- a/app/back-end/modules/import/import.js +++ b/app/back-end/modules/import/import.js @@ -97,6 +97,7 @@ class Import { this.parser.importTagsData(); this.parser.getImageURLs(); this.parser.importPostsData(); + this.parser.importPagesData(); this.parser.importImages(); } } diff --git a/app/back-end/modules/import/wxr-parser.js b/app/back-end/modules/import/wxr-parser.js index f15a3b493..76b5c8a0c 100644 --- a/app/back-end/modules/import/wxr-parser.js +++ b/app/back-end/modules/import/wxr-parser.js @@ -9,6 +9,7 @@ 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'); /** @@ -31,13 +32,15 @@ class WxrParser { this.temp = { authors: [], posts: [], + pages: [], tags: [], images: [], mapping: { authors: [], tags: [], images: [], - posts: [] + posts: [], + pages: [] }, imagesQueue: {} }; @@ -145,11 +148,11 @@ class WxrParser { items = items.filter(item => item['wp:post_type'] === filterType); } - if(items && items.length) { + if(items && (items.length || items.length === 0)) { return items.length; } - if(items) { + if(typeof items === 'object') { return 1; } @@ -375,7 +378,7 @@ class WxrParser { importPostsData() { let posts = this.parsedContent.rss.channel['item']; let newPost; - posts = posts ? posts.filter(item => this.postTypes.indexOf(item['wp:post_type']) !== -1) : false; + posts = posts ? posts.filter(item => this.postTypes.indexOf(item['wp:post_type']) !== -1 && item['wp:post_type'] !== 'page') : false; if(!posts) { return; @@ -490,6 +493,117 @@ class WxrParser { } } + /** + * Import pages data + */ + importPagesData() { + if (this.postTypes.indexOf('page') === -1) { + console.log('(!) Pages import is disabled'); + return; + } + + let pages = this.parsedContent.rss.channel['item']; + let newPage; + pages = pages ? pages.filter(item => item['wp:post_type'] === 'page') : false; + + if(!pages) { + console.log('(!) No pages to import'); + return; + } + + let untitledPagesCount = 1; + + console.log('(X) pages:', pages); + + for(let i = 0; i < pages.length; i++) { + if (!pages[i].title) { + console.log('(!) Empty page title detected - fallback to "Untitled #X" title'); + pages[i].title = 'Untitled #' + untitledPagesCount++; + } + + // For each page item insert post object + let pageImages = this.getPostImages(pages[i]['content:encoded']); + let pageSlug = slug(pages[i].title); + let pageAuthor = this.temp.authors[slug(pages[i]['dc:creator'])]; + let pageText = this.preparePostText(pages[i]['content:encoded'], pageImages); + let pageStatus = pages[i]['wp:status'] === 'draft' ? 'draft,is-page' : 'published,is-page' + let pageTitle = (pages[i].title).toString(); + + if(!this.importAuthors) { + pageAuthor = '1'; + } + + newPage = new Page(this.appInstance, { + id: 0, + site: this.siteName, + title: pageTitle, + slug: pageSlug, + author: pageAuthor, + status: pageStatus, + text: pageText, + creationDate: moment(pages[i]['wp:post_date']).format('x'), + modificationDate: moment().format('x'), + template: '', + additionalData: '', + pageViewSettings: '' + }, false); + + let newPageResult = newPage.save(); + let newPageID = newPageResult.postID; + + this.temp.pages[pageSlug] = newPageID; + this.temp.mapping.pages[pages[i]['wp:post_id']] = newPageID; + + // Create queue for download images + if(pageImages.length) { + this.temp.imagesQueue[newPageID] = pageImages; + } + + let featuredImage = this.getFeaturedPostImage(pages[i]); + let fileName = false; + + if(featuredImage) { + fileName = path.parse(featuredImage).base; + + if(!this.temp.imagesQueue[newPageID]) { + this.temp.imagesQueue[newPageID] = []; + } + + this.temp.imagesQueue[newPageID].push(featuredImage); + } + + if(fileName) { + let featuredPageImageSqlQuery = newPage.db.prepare(`INSERT INTO posts_images VALUES(NULL, @newPageID, @fileName, '', '', @config)`); + featuredPageImageSqlQuery.run({ + newPageID: newPageID, + fileName: fileName, + config: '{"alt":"","caption":"","credits":""}' + }); + + let featuredPageID = newPage.db.prepare('SELECT last_insert_rowid() AS id').get().id; + let featuredPageIdUpdate = newPage.db.prepare(`UPDATE posts SET featured_image_id = @featuredPostID WHERE id = @newPageID`); + + featuredPageIdUpdate.run({ + featuredPageID, + newPageID + }); + } + + process.send({ + type: 'progress', + message: { + translation: 'core.wpImport.pagesProgressInfo', + translationVars: { + progress: (i + 1), + total: pages.length + } + } + }); + + console.log('-> Imported page (' + (i+1) + ' / ' + pages.length + '): ' + pageTitle); + } + } + /** * Create array with all available images for download */ diff --git a/app/back-end/modules/render-html/contexts/404.js b/app/back-end/modules/render-html/contexts/404.js index c831d8308..4096c761e 100644 --- a/app/back-end/modules/render-html/contexts/404.js +++ b/app/back-end/modules/render-html/contexts/404.js @@ -24,6 +24,7 @@ class RendererContext404 extends RendererContext { 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); @@ -34,18 +35,26 @@ class RendererContext404 extends RendererContext { 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() { - let self = this; 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]); } /** @@ -66,6 +75,7 @@ class RendererContext404 extends RendererContext { featuredPosts: this.featuredPosts, hiddenPosts: this.hiddenPosts, tags: this.tags, + pages: this.pages, mainTags: this.mainTags, authors: this.authors, metaTitleRaw: this.metaTitle, diff --git a/app/back-end/modules/render-html/contexts/author.js b/app/back-end/modules/render-html/contexts/author.js index 3eeb6e5ce..dfa035ffc 100644 --- a/app/back-end/modules/render-html/contexts/author.js +++ b/app/back-end/modules/render-html/contexts/author.js @@ -43,6 +43,7 @@ class RendererContextAuthor extends RendererContext { 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 @@ -65,6 +66,14 @@ class RendererContextAuthor extends RendererContext { 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() { @@ -75,6 +84,8 @@ class RendererContextAuthor extends RendererContext { 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); @@ -149,6 +160,7 @@ class RendererContextAuthor extends RendererContext { 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, diff --git a/app/back-end/modules/render-html/contexts/feed.js b/app/back-end/modules/render-html/contexts/feed.js index c826d3a5e..27d2d17f1 100644 --- a/app/back-end/modules/render-html/contexts/feed.js +++ b/app/back-end/modules/render-html/contexts/feed.js @@ -33,6 +33,7 @@ class RendererContextFeed extends RendererContext { status LIKE '%published%' AND ${featuredPostsCondition} status NOT LIKE '%hidden%' AND + status NOT LIKE '%is-page%' AND status NOT LIKE '%trashed%' ORDER BY created_at DESC diff --git a/app/back-end/modules/render-html/contexts/home.js b/app/back-end/modules/render-html/contexts/home.js index 0bd5abf40..98a004a95 100644 --- a/app/back-end/modules/render-html/contexts/home.js +++ b/app/back-end/modules/render-html/contexts/home.js @@ -38,6 +38,7 @@ class RendererContextHome extends RendererContext { 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} @@ -65,8 +66,16 @@ class RendererContextHome extends RendererContext { 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() { @@ -77,6 +86,8 @@ class RendererContextHome extends RendererContext { 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); @@ -104,6 +115,7 @@ class RendererContextHome extends RendererContext { hiddenPosts: this.hiddenPosts, tags: this.tags, mainTags: this.mainTags, + pages: this.pages, authors: this.authors, metaTitleRaw: this.metaTitle, metaDescriptionRaw: this.metaDescription, @@ -136,12 +148,10 @@ class RendererContextHome extends RendererContext { FROM posts WHERE - status LIKE '%published%' - AND - status NOT LIKE '%hidden%' - AND - status NOT LIKE '%trashed%' - AND + 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 diff --git a/app/back-end/modules/render-html/contexts/page-preview.js b/app/back-end/modules/render-html/contexts/page-preview.js new file mode 100644 index 000000000..a56b49e54 --- /dev/null +++ b/app/back-end/modules/render-html/contexts/page-preview.js @@ -0,0 +1,373 @@ +// 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(/\
<\/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(/