Merge branch 'staging' into http-upload

This commit is contained in:
yflory 2025-05-21 15:17:56 +02:00
commit 61cc1f5f86
14 changed files with 465 additions and 38 deletions

View File

@ -1,10 +0,0 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
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 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.
For more information, please refer to <http://unlicense.org/>

View File

@ -1,3 +1,9 @@
/*
* SPDX-FileCopyrightText: 2025 XWiki CryptPad Team <contact@cryptpad.org> and contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@import (reference) "./colortheme-all.less";
@import (reference) "./variables.less";
.badges_vars(

View File

@ -104,6 +104,7 @@ server {
# Enable SharedArrayBuffer in Firefox (for .xlsx export)
add_header Cross-Origin-Resource-Policy cross-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
add_header Referrer-Policy same-origin;
# Insert the path to your CryptPad repository root here
root /home/cryptpad/cryptpad;
@ -169,6 +170,7 @@ server {
# they unfortunately still require exceptions to the sandboxing to work correctly.
if ($uri ~ ^/(?:sheet|doc|presentation)/inner.html) { set $unsafe 1; }
if ($uri ~ ^/common/onlyoffice/.*/.*\.html) { set $unsafe 1; }
if ($uri ~ ^/common/onlyoffice/dist/.*/sdkjs/common/spell/spell/spell.js.*$) { set $unsafe 1; }
# everything except the sandbox domain is a privileged scope, as they might be used to handle keys
if ($host != $sandbox_domain) { set $unsafe 0; }

View File

@ -28,7 +28,7 @@ main() {
ask_for_license
# Check if 'oldest_needed_version' is already set, if not, set it to v8
if [ -z "${PROPS['oldest_needed_version']}" ]; then
if [ -z "${PROPS['oldest_needed_version']+set}" ]; then
echo "'oldest_needed_version' is not set. Setting it to v8."
set_prop "oldest_needed_version" "v8"
else
@ -84,7 +84,7 @@ main() {
rm -rf "$OO_DIR/v6/web-apps/apps/spreadsheeteditor/main/resources/help"
;;
v7)
install_version v7 v7.3.3.60+10 be3c926d534b0c77aa1ed83b170a38b9e56ebbbfd73a1a968f523d63c5ed69ecdb063fad08740812de82c1e79af7309faf67ffdd81b45451adce539192f3414c
install_version v7 v7.3.3.60+11 1e65be6dc87d97e82b4972f303956e5397b34d637ca80a4239c48e49ab829ee5afc8f5b1680b2fb14230d63ff872ec5f9b562bb6c3f1811316b68f8b436f7ee6
rm -rf "$OO_DIR/v7/web-apps/apps/documenteditor/main/resources/help"
rm -rf "$OO_DIR/v7/web-apps/apps/presentationeditor/main/resources/help"
rm -rf "$OO_DIR/v7/web-apps/apps/spreadsheeteditor/main/resources/help"
@ -92,7 +92,7 @@ main() {
# From all the older versions only v7 has 'dictionaries', we remove it for the same reasons
rm -rf "$OO_DIR/v7/dictionaries/"
;;
v8) install_version v8 v8.3.3.23+1 b9647468ef26c915788da3b2528e0496ba5fa78d5e7acb85eca11241cc325478d94f5508fdcf085b42adf0cad8eda830d71be87c58918d1f01a86c979da6f4e5 ;;
v8) install_version v8 v8.3.3.23+3 6a00704df88c87bf5810257d99e83b32b76b86326ebd668b09c257f0abc283991b9ec3c121e0801173afc4b8deb21d4539c307da26e40e42f6d4f3af195526c2 ;;
x2t) install_x2t v7.3+1 ab0c05b0e4c81071acea83f0c6a8e75f5870c360ec4abc4af09105dd9b52264af9711ec0b7020e87095193ac9b6e20305e446f2321a541f743626a598e5318c1 ;;
*)
echo "Unknown version: $version"

View File

@ -57,6 +57,7 @@ Default.httpHeaders = function (Env) {
"X-XSS-Protection": "1; mode=block",
"X-Content-Type-Options": "nosniff",
"Access-Control-Allow-Origin": Env.enableEmbedding? '*': Env.permittedEmbedders,
"Referrer-Policy": "same-origin",
"Permissions-policy":"interest-cohort=()"
};
};

View File

@ -658,6 +658,10 @@ const factory = (Sortify, UserObject, ProxyManager,
if (data.password) { pad.password = data.password; }
if (data.channel || secret) { pad.channel = data.channel || secret.channel; }
if (data.readme) { pad.readme = 1; }
Object.keys(data.attributes || {}).forEach(k => {
if (!data.attributes[k]) { return; } // undefined
pad[k] = data.attributes[k];
});
if (data.teamId === -1) { data.teamId = undefined; }
var s = getStore(data.teamId);
@ -1212,7 +1216,8 @@ const factory = (Sortify, UserObject, ProxyManager,
owners: owners,
expire: expire,
password: data.password,
path: data.path
path: data.path,
attributes: data.attributes
}, cb);
// Let inner know that dropped files shouldn't trigger the popup
postMessage(clientId, "AUTOSTORE_DISPLAY_POPUP", {
@ -2282,6 +2287,14 @@ const factory = (Sortify, UserObject, ProxyManager,
}
};
const startCacheModules = (clientId, returned, _cb) => {
const cb = Util.mkAsync(_cb);
onCacheReady(clientId, function () {
onCacheReadyEvt.fire();
cb(returned);
});
};
// onReady: called when the drive is synced (not using the cache anymore)
// "cb" is wrapped in Util.once() and may have already been called
// if we have a local cache
@ -2656,13 +2669,6 @@ const factory = (Sortify, UserObject, ProxyManager,
andThen();
};
const startCacheModules = (clientId, returned, _cb) => {
const cb = Util.mkAsync(_cb);
onCacheReady(clientId, function () {
onCacheReadyEvt.fire();
cb(returned);
});
};
const startModules = (clientId, returned, cb) => {
if (store.manager) {
return void onCacheReadyEvt.reg(function () {

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2025 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import nacl from 'tweetnacl/nacl-fast';
import nThen from 'nthen';
import { Module, ModuleObject, Callback } from '../types'

View File

@ -112,6 +112,7 @@ const factory = () => {
txid: txid,
lastKnownHash: chan.lastKnownHash || chan.lastCpHash,
metadata: {
forcePlaceholder: true,
validateKey: obj.validateKey,
owners: obj.owners,
expire: obj.expire
@ -157,7 +158,10 @@ const factory = () => {
ctx.emit('READY', chan.clients, chan.clients);
return;
}
if (parsed.error && parsed.channel) { return; }
if (parsed.error && parsed.channel) {
ctx.emit('READY', chan.clients, chan.clients);
return;
}
// If there is a txid, make sure it's ours or abort
if (Array.isArray(parsed) && parsed[0] && parsed[0] !== txid) {

View File

@ -1145,6 +1145,13 @@ define([
}
}
// Make sure we also store additionnal data to pin all the channels
// (OO and forms)
data.attributes = {};
Object.keys(common?.otherPadAttrs || {}).forEach(k => {
data.attributes[k] = common.otherPadAttrs[k];
});
postMessage("SET_PAD_TITLE", data, function (obj) {
if (obj && obj.error) {
if (obj.error !== "EAUTH") { console.log("unable to set pad title"); }
@ -1167,6 +1174,8 @@ define([
return;
}
let rtChannel, lastVersion, answersChannel;
let attributes = {};
nThen(function (waitFor) {
if (parsed.hashData.type !== 'pad') { return; }
// Set the correct owner and expiration time if we can find them
@ -1177,6 +1186,14 @@ define([
data.owners = obj.owners;
data.expire = +obj.expire;
}));
common.getPadAttribute('', waitFor(function (err, _data) {
attributes.rtChannel = _data?.rtChannel;
attributes.lastVersion = _data?.lastVersion;
attributes.answersChannel = _data?.answersChannel;
Object.keys(common?.otherPadAttrs || {}).forEach(k => {
attributes[k] = common.otherPadAttrs[k];
});
}), data.href);
}).nThen(function () {
postMessage("SET_PAD_TITLE", {
teamId: data.teamId,
@ -1187,6 +1204,7 @@ define([
path: data.path,
owners: data.owners,
expire: data.expire,
attributes,
forceSave: 1
}, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }

View File

@ -22,6 +22,7 @@ define([
'/common/onlyoffice/oocell_base.js',
'/common/onlyoffice/oodoc_base.js',
'/common/onlyoffice/ooslide_base.js',
'/common/outer/worker-channel.js',
'/common/onlyoffice/current-version.js',
'/common/onlyoffice/broken-formats.js',
@ -50,10 +51,12 @@ define([
EmptyCell,
EmptyDoc,
EmptySlide,
Channel,
OOCurrentVersion,
BrokenFormats)
{
var saveAs = window.saveAs;
var Nacl = window.nacl;
var APP = window.APP = {
$: $,
urlArgs: Util.find(ApiConfig, ['requireConf', 'urlArgs'])
@ -1087,6 +1090,71 @@ define([
}
};
var handleAuth = function (obj, send) {
//setEditable(false);
var changes = [];
if (content.version > 2) {
ooChannel.queue.forEach(function (data) {
Array.prototype.push.apply(changes, data.msg.changes);
});
ooChannel.ready = true;
ooChannel.cpIndex += ooChannel.queue.length;
var last = ooChannel.queue.pop();
if (last) { ooChannel.lastHash = last.hash; }
} else {
setEditable(false, true);
}
send({
type: "authChanges",
changes: changes
});
// Answer to the auth command
var p = getParticipants();
send({
type: "auth",
result: 1,
sessionId: "sessionId",
participants: p.list,
locks: [],
changes: [],
changesIndex: 0,
indexUser: p.index,
buildVersion: "5.2.6",
buildNumber: 2,
licenseType: 3,
//"g_cAscSpellCheckUrl": "/spellchecker",
//"settings":{"spellcheckerUrl":"/spellchecker","reconnection":{"attempts":50,"delay":2000}}
});
// Open the document
send({
type: "documentOpen",
data: {"type":"open","status":"ok","data":{"Editor.bin":obj.openCmd.url}}
});
/*
// TODO: make sure we don't have new popups that can break our integration
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === "childList") {
for (var i = 0; i < mutation.addedNodes.length; i++) {
if (mutation.addedNodes[i].classList.contains('asc-window') &&
mutation.addedNodes[i].classList.contains('alert')) {
$(mutation.addedNodes[i]).find('button').not('.custom').click();
}
}
}
});
});
observer.observe(window.frames[0].document.body, {
childList: true,
});
*/
};
var handleLock = function (obj, send) {
if (APP.history) { return; }
@ -1557,6 +1625,227 @@ define([
}
};
var makeChannel = function () {
var msgEv = Util.mkEvent();
var iframe = $('#cp-app-oo-editor > iframe')[0].contentWindow;
var type = common.getMetadataMgr().getPrivateData().ooType;
window.addEventListener('message', function (msg) {
if (msg.source !== iframe) { return; }
msgEv.fire(msg);
});
var postMsg = function (data) {
iframe.postMessage(data, ApiConfig.httpSafeOrigin);
};
Channel.create(msgEv, postMsg, function (chan) {
APP.chan = chan;
var send = ooChannel.send = function (obj, force) {
// can't push to OO before reloading cp
if (APP.onStrictSaveChanges && !force) { return; }
// We only need to release locks for sheets
if (type !== "sheet" && obj.type === "releaseLock") { return; }
if (type === "presentation" && obj.type === "cp_theme") {
console.error(obj);
return;
}
debug(obj, 'toOO');
chan.event('CMD', obj);
if (obj && obj.type === "saveChanges") {
evIntegrationSave.fire();
}
};
chan.on('CMD', function (obj) {
debug(obj, 'fromOO');
switch (obj.type) {
case "auth":
handleAuth(obj, send);
break;
case "isSaveLock":
// TODO ping the server to check if we're online first?
if (!offline) {
if (APP.waitLock) {
APP.waitLock.reg(function () {
send({
type: "saveLock",
saveLock: false
}, true);
});
} else {
send({
type: "saveLock",
saveLock: false
}, true);
}
}
break;
case "cursor":
if (cursor && cursor.updateCursor) {
cursor.updateCursor({
type: "cursor",
messages: [{
cursor: obj.cursor,
time: +new Date(),
user: myUniqueOOId,
useridoriginal: myOOId
}]
});
}
break;
case "forceSaveStart":
if (APP.integrationSave) {
APP.integrationSave(obj => {
if (obj?.error) {
console.error(obj.error);
return void UI.warn(Messages.error);
}
content.integrationSave = `${myUniqueOOId}-${+new Date()}`;
APP.integrationSaved = content.integrationSave;
APP.onLocal();
UI.log(Messages.saved);
});
}
break;
case "getLock":
handleLock(obj, send);
break;
case "getMessages":
// OO chat messages?
send({ type: "message" });
break;
case "saveChanges":
if (readOnly) {
return;
}
// If we have unsaved data before reloading for a checkpoint...
if (APP.onStrictSaveChanges) {
delete APP.unsavedLocks;
APP.unsavedChanges = {
type: "saveChanges",
changes: parseChanges(obj.changes),
changesIndex: ooChannel.cpIndex || 0,
locks: type === "sheet" ? [] : APP.unsavedLocks,
excelAdditionalInfo: null,
recover: true
};
APP.onStrictSaveChanges();
return;
}
var AscCommon = window.frames[0] && window.frames[0].AscCommon;
if (Util.find(AscCommon, ['CollaborativeEditing','m_bFast'])
&& APP.themeLocked) {
obj = APP.themeLocked;
APP.themeLocked = undefined;
obj.type = "cp_theme";
console.error(obj);
}
if (APP.themeRemote) {
delete APP.themeRemote;
send({
type: "unSaveLock",
index: ooChannel.cpIndex,
time: +new Date()
});
return;
}
// We're sending our changes to netflux
handleChanges(obj, send);
// If we're alone, clean up the medias
var m = metadataMgr.getChannelMembers().slice().filter(function (nId) {
return nId.length === 32;
});
if (m.length === 1 && APP.loadingImage <= 0) {
try {
// "docs" contains the correct images that we've just uploaded
// "docs2" contains the correct images from the .bin checkpoint
// both of them are not reliable in the other case
var docs = getWindow().AscCommon.g_oDocumentUrls.urls;
var docs2 = getEditor().ImageLoader.map_image_index;
var mediasSources = getMediasSources();
Object.keys(mediasSources).forEach(function (name) {
if (!docs && !docs2) { return; }
if (!docs['media/'+name] && !docs2[name]) {
delete mediasSources[name];
}
});
APP.onLocal();
} catch (e) {}
}
break;
case "unLockDocument":
if (obj.releaseLocks && content.locks && content.locks[getId()]) {
send({
type: "releaseLock",
locks: getUserLock(getId())
});
delete content.locks[getId()];
APP.onLocal();
}
if (obj.isSave) {
send({
type: "unSaveLock",
time: -1,
index: -1
});
}
if (APP.onDocumentUnlock) {
APP.onDocumentUnlock();
APP.onDocumentUnlock = undefined;
}
break;
case 'openDocument':
// When duplicating a slide, OO may ask the URLs of the images
// in that slide
var _obj = obj.message;
if (_obj.c === "imgurls") {
var _mediasSources = getMediasSources();
var images = _obj.data || [];
if (!Array.isArray(images)) { return; }
var urls = [];
nThen(function (waitFor) {
images.forEach(function (name) {
if (/^data\:image/.test(name)) {
Util.fetch(name, waitFor(function (err, u8) {
if (err) { return; }
var b = new Blob([u8]);
urls.push(URL.createObjectURL(b));
}));
return;
}
var data = _mediasSources[name];
if (!data) { return; }
var media = mediasData[data.src];
if (!media) { return; }
urls.push({
path: name,
url: media.blobUrl,
});
});
}).nThen(function () {
send({
type: "documentOpen",
data: {
type: "imgurls",
status: "ok",
data: {
urls: urls,
error: 0
}
}
});
});
}
break;
}
});
});
};
var x2tConvertData = function (data, fileName, format, cb) {
var sframeChan = common.getSframeChannel();
var editor = getEditor();
@ -2145,6 +2434,98 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
};
APP.loadingImage = 0;
APP.getImageURL = function(name, callback) {
if (name && /^data:image/.test(name)) {
return void callback('');
}
var mediasSources = getMediasSources();
var data = mediasSources[name];
downloadImages[name] = Util.mkEvent(true);
if (typeof data === 'undefined') {
if (/^http/.test(name) && /slide\/themes\/theme/.test(name)) {
Util.fetch(name, function (err, u8) {
if (err) { return; }
mediasData[name] = {
blobUrl: name,
content: u8,
name: name
};
var b = new Blob([u8], {type: "image/jpeg"});
var blobUrl = URL.createObjectURL(b);
return void callback(blobUrl);
});
return;
}
debug("CryptPad - could not find matching media for " + name);
return void callback("");
}
var blobUrl = (typeof mediasData[data.src] === 'undefined') ? "" : mediasData[data.src].blobUrl;
if (blobUrl) {
delete downloadImages[name];
debug("CryptPad Image already loaded " + blobUrl);
// Fix: https://github.com/cryptpad/cryptpad/issues/1500
// Maybe OO was reloaded, but the CryptPad cache is still intact?
// -> Add the image to OnlyOffice again.
const documentUrls = window.frames[0].AscCommon.g_oDocumentUrls;
if (!(data.name in documentUrls.getUrls())) {
documentUrls.addImageUrl(data.name, blobUrl);
}
return void callback(blobUrl);
}
APP.loadingImage++;
Util.fetch(data.src, function (err, u8) {
if (err) {
APP.loadingImage--;
console.error(err);
return void callback("");
}
try {
debug("Decrypt with key " + data.key);
FileCrypto.decrypt(u8, Nacl.util.decodeBase64(data.key), function (err, res) {
APP.loadingImage--;
if (err || !res.content) {
debug("Decrypting failed");
return void callback("");
}
try {
var blobUrl = URL.createObjectURL(res.content);
// store media blobUrl and content for cache and export
var mediaData = {
blobUrl : blobUrl,
content : "",
name: name
};
mediasData[data.src] = mediaData;
var reader = new FileReader();
reader.onloadend = function () {
debug("MediaData set");
mediaData.content = reader.result;
downloadImages[name].fire();
};
reader.readAsArrayBuffer(res.content);
debug("Adding CryptPad Image " + data.name + ": " + blobUrl);
window.frames[0].AscCommon.g_oDocumentUrls.addImageUrl(data.name, blobUrl);
callback(blobUrl);
} catch (e) {}
});
} catch (e) {
APP.loadingImage--;
debug("Exception decrypting image " + data.name);
console.error(e);
callback("");
}
}, void 0, common.getCache());
};
// Always hide right menu
try {
localStorage?.original?.removeItem('sse-hide-right-settings');
@ -2155,10 +2536,10 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
APP.docEditor = new window.DocsAPI.DocEditor("cp-app-oo-placeholder-a", APP.ooconfig);
ooLoaded = true;
// TODO move this back to CryptPad
// if (content.version < 7) {
// APP.docEditor.installLegacyChannel();
// }
if (content.version < 7) {
makeChannel();
return;
}
APP.docEditor.connectMockServer({
onMessage: fromOOHandler,
getParticipants: getParticipants,
@ -2654,7 +3035,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
(content.version <= 3 ? 'v2b/' : OOCurrentVersion.currentVersion + '/');
var s = h('script', {
type:'text/javascript',
src: ApiConfig.httpSafeOrigin + '/common/onlyoffice/dist/'+version+'web-apps/apps/api/documents/api.js'
src: ApiConfig.httpSafeOrigin + '/common/onlyoffice/dist/'+version+'web-apps/apps/api/documents/api.js?' + APP.urlArgs
});
$('#cp-app-oo-editor').empty().append(h('div#cp-app-oo-placeholder-a')).append(s);
@ -3112,7 +3493,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
var s = h('script', {
type:'text/javascript',
src: ApiConfig.httpSafeOrigin + '/common/onlyoffice/dist/'+version+'web-apps/apps/api/documents/api.js'
src: ApiConfig.httpSafeOrigin + '/common/onlyoffice/dist/'+version+'web-apps/apps/api/documents/api.js?' + APP.urlArgs
});
$('#cp-app-oo-editor').append(s);

View File

@ -46,20 +46,32 @@ define([
sframeChan.on('Q_OO_SAVE', function (data, cb) {
var chanId = Utils.Hash.hrefToHexChannelId(data.url);
Cryptpad.getPadAttribute('lastVersion', function (err, data) {
if (data) {
var oldChanId = Utils.Hash.hrefToHexChannelId(data);
if (oldChanId !== chanId) { Cryptpad.unpinPads([oldChanId], function () {}); }
}
if (!data) { return; }
var oldChanId = Utils.Hash.hrefToHexChannelId(data);
if (oldChanId !== chanId) { Cryptpad.unpinPads([oldChanId], function () {}); }
});
Cryptpad.pinPads([chanId], function (e) {
if (e) { return void cb(e); }
Cryptpad.setPadAttribute('lastVersion', data.url, cb);
channels.lastVersion = data.url;
// If pad is stored, pin
Cryptpad.getPadAttribute('channel', function (err, data) {
if (err || !data) { return; }
Cryptpad.pinPads([chanId], function (e) {
if (e) { return void cb(e); }
});
});
Cryptpad.setPadAttribute('lastVersion', data.url, cb);
channels.lastVersion = data.url;
Cryptpad.setPadAttribute('lastCpHash', data.hash, cb);
channels.lastCpHash = data.hash;
});
sframeChan.on('Q_OO_OPENCHANNEL', function (data, cb) {
const other = Cryptpad.otherPadAttrs = {
rtChannel: data.channel
};
if (channels.lastVersion) {
other.lastVersion = channels.lastVersion;
}
other.lastCpHash = channels.lastCpHash;
Cryptpad.getPadAttribute('rtChannel', function (err, res) {
// If already stored, don't pin it again
if (res && res === data.channel) { return; }

View File

@ -319,7 +319,7 @@ define([
let canNoDrive = false;
try {
var parsed = Utils.Hash.parsePadUrl(currentPad.href);
canNoDrive = !(parsed?.hashData?.version === 3) && !parsed?.hashData?.password;
canNoDrive = ![3,4].includes(parsed?.hashData?.version) && !parsed?.hashData?.password;
var options = parsed.getOptions();
if (options.loginOpts) {
var loginOpts = Utils.Hash.decodeDataOptions(options.loginOpts);

File diff suppressed because one or more lines are too long

View File

@ -45,6 +45,9 @@ define([
var addRpc = function (sframeChan, Cryptpad) {
sframeChan.on('EV_FORM_PIN', function (data) {
channels.answersChannel = data.channel;
Cryptpad.otherPadAttrs = {
answersChannel: data.channel
};
Cryptpad.changeMetadata();
Cryptpad.getPadAttribute('answersChannel', function (err, res) {
// If already stored, don't pin it again