Skip to content

Instantly share code, notes, and snippets.

@crutch12
Last active November 9, 2023 14:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save crutch12/2c94ddc0c2070e0b6e613ea431c2f215 to your computer and use it in GitHub Desktop.
Save crutch12/2c94ddc0c2070e0b6e613ea431c2f215 to your computer and use it in GitHub Desktop.
Offline mode (page dumps) for confluence-server@7.5.0

Offline mode (page dumps) for confluence-server

Confluence Server only. It will not work with Confluence Cloud

Supports all modern browsers.

Why

When you are editing page content in confluence it continuously saves data to server. But if you lost internet connection (for a long time) in the middle of the path and then closed your browser - you will lose all unsaved data.

This script is created to dump unsaved data and allow you restore that dump.

Installation

Preffered way

Unsafe way

  • Go to Admin - Layouts - Main Layout (Create custom or Edit) - Add <script src="https://gistcdn.githack.com/crutch12/2c94ddc0c2070e0b6e613ea431c2f215/raw/f47e427dc73c9d4db4b5f73848210a48113cf470/offline.js" async></script> in <head> section
  • Done

How it works

Every 30 seconds this script checks current page.

If

  • it's editing page
  • you have changed some content (it's different from previous dump)

then this script dumps new data in IndexedDB.

So sometimes you should manually clear IndexedDB to clean out of extra trash dumps (it's not required)

How to restore data

Guide

  1. Open confluence
  2. Open F12 - Console, Run:
await __getSavedOfflineData().then(data => data.sort((a, b) => a.value.date - b.value.date))
  1. Find needed element (sorted by date) - look at key

Example - mgmt/2023/09/29 Протокол встречи лидов/mgmt/p-142835875/d-142835877/v-11 Meaning:

  • mgmt (Management) - space
  • 2023/09/29 Протокол встречи лидов - title
  • p-142835875 - page id
  • d-142835877 - page id + version
  • v-11 - last edited version

Look at value

  • date - save date
  • value - page content (html)
  1. Find needed element, copy content

Go to link, paste text in left side, press Run https://www.w3schools.com/html/tryit.asp?filename=tryhtml_editor

  1. Copy result from righ side, go to Confluence, paste copied resut.

  2. Done

How to check saved data

Go to

F12 (DevTools) - tab Application - IndexedDB (Storage section) - iframe/wysiwyg/body - wysiwyg

Now just find needed key and read value field.

Example:

image

Caveats

If you lost your data and want to restore it with dump in IndexedDB, then you shouldn't edit (typing) page after data loss.

You may click "Edit", but do not type anything, otherwise the saved dump will be rewrited in 30sec and you will lose saved data.

/*!
* Offline mode for confluence-server@7.5.0
* https://hub.docker.com/r/atlassian/confluence-server/tags?page=1&name=7.5.0
*
* Source code
* https://gist.github.com/crutch12/2c94ddc0c2070e0b6e613ea431c2f215
*
* @author Konstantin Barabanov <criitch@yandex.ru>
*
* Date: 2023-01-24T21:28Z
*/
(() => {
const INTERVAL = 30000;
addEventListener('DOMContentLoaded', () => {
console.debug('offline.js started...')
// @NOTE: Every INTERVAL seconds check and save changes
setInterval(() => {
const editMode = document.body.classList.contains('contenteditor') || document.body.classList.contains('edit') || document.body.classList.contains('create')
if (!editMode) return;
const content = getContent()
if (!content) return;
const info = getInfo();
saveContent({
...info,
content,
}).catch(err => console.error(err))
}, INTERVAL)
});
const getMetaTagContent = (name) => {
const tag = document.querySelector(`meta[name="${name}"]`)
return tag ? tag.content : undefined
}
const getInfo = () => {
const draftId = getMetaTagContent('ajs-draft-id')
const pageId = getMetaTagContent('ajs-page-id')
const pageVersion = getMetaTagContent('page-version')
const spaceKey = getMetaTagContent('ajs-space-key')
const type = getMetaTagContent('ajs-space-key')
const title = document.querySelector('#content-title').value || getMetaTagContent('ajs-latest-published-page-title') || getMetaTagContent('ajs-page-title')
// <input id="syncRev" type="hidden" name="syncRev" value="0.kHzKSow3znE6tLEeBFN4xTo.36"></input>
const syncRev = document.querySelector('#syncRev').value // @NOTE: it changes, when you edit page content (e.g. typing smth)
return {
spaceKey,
type,
draftId,
pageId,
title,
pageVersion,
syncRev,
}
}
const getContent = () => {
// @NOTE: wysiwyg iframe
const wysiwyg = document.querySelector('#wysiwyg iframe')
if (!wysiwyg) return;
const doc = wysiwyg.contentDocument || wysiwyg.contentWindow.document
const content = doc.body.innerHTML
return content;
}
// @NOTE: Save data (content + date + syncRev)
const saveContent = async (data) => {
if (!db) await openDB();
const key = [data.spaceKey, data.title, data.type, 'p-' + data.pageId, 'd-' + data.draftId, 'v-' + data.pageVersion].join('/')
const value = data.content
const syncRev = data.syncRev
const saved = await getFromStore(key).catch(() => null)
if (!saved) return addToStore(key, value, syncRev)
// @NOTE: Rewrite only of syncRev changed -> content changed
if (saved.syncRev !== syncRev) {
return addToStore(key, value, syncRev)
}
}
// @NOTE: Edit page
/* <body id="com-atlassian-confluence" class="theme-default contenteditor edit aui-layout aui-theme-default synchrony-active aui8 cw vsc-initialized" data-aui-version="8.3.5"></body> */
// @NOTE: warning message, if couldn't save data to server:
/* <div class="status-indicator-icon aui-icon aui-icon-small aui-iconfont-warning" data-tooltip="Can't reach the server. Check your internet connection, and we'll keep trying to reconnect you."></div> */
// storage logic
const dbName = "iframe/wysiwyg/body";
const version = 1; // incremental ints
const storeName = "wysiwyg";
let db = null; // define the db variable to be global in the sw file
function openDB() {
return new Promise((resolve, reject) => {
// ask to open the db
const openRequest = self.indexedDB.open(dbName, version);
openRequest.onerror = function (event) {
console.debug(
"Everyhour isn't allowed to use IndexedDB?!" + event.target.errorCode
);
db = null;
reject(event)
};
// upgrade needed is called when there is a new version of you db schema that has been defined
openRequest.onupgradeneeded = function (event) {
db = event.target.result;
if (!db.objectStoreNames.contains(storeName)) {
// if there's no store of 'storeName' create a new object store
db.createObjectStore(storeName, { keyPath: "key" }); //some use keyPath: "id" (basically the primary key) - unsure why yet
}
};
openRequest.onsuccess = function (event) {
db = event.target.result;
resolve(db)
};
})
}
function addToStore(key, value, syncRev) {
return new Promise((resolve, reject) => {
// start a transaction of actions you want to submit
const transaction = db.transaction(storeName, "readwrite");
// create an object store
const store = transaction.objectStore(storeName);
// add key and value to the store
const request = store.put({ date: new Date(), value, key, syncRev });
request.onsuccess = function () {
resolve(request.result)
};
request.onerror = function () {
console.debug("Error did not save to store", request.error);
reject(request.error)
};
transaction.onerror = function (event) {
console.debug("trans failed", event);
reject(request.result)
};
transaction.oncomplete = function (event) {
resolve(request.event)
};
})
}
function getFromStore(key) {
return new Promise((resolve, reject) => {
// start a transaction
const transaction = db.transaction(storeName, "readwrite");
// create an object store
const store = transaction.objectStore(storeName);
// get key and value from the store
const request = store.get(key);
request.onsuccess = function (event) {
resolve(event.target.result) // { key: string, value: string, date: Date, syncRev: string } structure
};
request.onerror = function () {
console.debug("Error did not read to store", request.error);
reject(request.error)
};
transaction.onerror = function (event) {
console.debug("trans failed", event);
reject(request.event)
};
transaction.oncomplete = function (event) {
resolve(request.event)
};
})
}
window.__getSavedOfflineData = async () => {
if (!db) await openDB();
return new Promise((res, rej) => {
// Fetch keys
const keysTr = db.transaction(storeName).objectStore(storeName).getAllKeys()
keysTr.onsuccess = (event) => {
const keys = event.target.result
if (keys?.length) {
// Start a new transaction for final result
const valuesTr = db.transaction(storeName)
const objStore = valuesTr.objectStore(storeName)
const result = [] // { key, value }[]
// Iterate over keys
keys.forEach(key => {
const tr = objStore.get(key)
tr.onsuccess = e => {
result.push({
key,
value: e.target.result
})
}
})
// Resolve `getAll` with final { key, value }[] result
valuesTr.oncomplete = (event) => {
res(result)
}
valuesTr.onerror = (event) => {
rej(event)
}
}
else
res([])
}
keysTr.onerror = (event) => {
rej(event)
}
})
}
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment