Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block cloning from brew metadata #3637

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
29 changes: 28 additions & 1 deletion client/homebrew/editor/metadataEditor/metadataEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const MetadataEditor = createClass({
systems : [],
renderer : 'legacy',
theme : '5ePHB',
lang : 'en'
lang : 'en',
cloning : true,
},
onChange : ()=>{},
reportError : ()=>{}
Expand Down Expand Up @@ -102,6 +103,12 @@ const MetadataEditor = createClass({
}
this.props.onChange(this.props.metadata, 'renderer');
},

handleCloning: function(e) {
const newMetadata = { ...this.props.metadata, cloning: e.target.checked };
this.props.onChange(newMetadata, 'cloning');
},

handlePublish : function(val){
this.props.onChange({
...this.props.metadata,
Expand Down Expand Up @@ -312,6 +319,24 @@ const MetadataEditor = createClass({
</div>;
},

renderCloning : function(){
return <div className="field cloning">
<label>Cloning</label>
<div className="value">
<label>
<input
type='checkbox'
name='clone'
checked={this.props.metadata.cloning}
onChange={(e)=>this.handleCloning(e)}/>
Allow this brew to be cloned
</label>
</div>

</div>

},

render : function(){
return <div className='metadataEditor'>
<h1 className='sectionHead'>Brew</h1>
Expand Down Expand Up @@ -379,6 +404,8 @@ const MetadataEditor = createClass({

<h1 className='sectionHead'>Privacy</h1>

{this.renderCloning()}

<div className='field publish'>
<label>publish</label>
<div className='value'>
Expand Down
14 changes: 14 additions & 0 deletions client/homebrew/editor/metadataEditor/metadataEditor.less
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,20 @@
cursor : pointer;
}
}

.cloning.field .value{
label {
display : inline-flex;
align-items : center;
margin-right : 15px;
font-size : 0.7em;
font-weight : 800;
white-space : nowrap;
vertical-align : middle;
cursor : pointer;
user-select : none;
}
}
.publish.field .value {
position : relative;
margin-bottom : 15px;
Expand Down
2 changes: 2 additions & 0 deletions client/homebrew/homebrew.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const SharePage = require('./pages/sharePage/sharePage.jsx');
const NewPage = require('./pages/newPage/newPage.jsx');
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
const AccountPage = require('./pages/accountPage/accountPage.jsx');
const SourcePage = require('./pages/sourcePage/sourcePage.jsx');

const WithRoute = (props)=>{
const params = useParams();
Expand Down Expand Up @@ -68,6 +69,7 @@ const Homebrew = createClass({
<Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/source/:id' element={<WithRoute el={SourcePage} brew={this.props.brew} />} />
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
Expand Down
19 changes: 19 additions & 0 deletions client/homebrew/pages/errorPage/errors/errorIndex.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,25 @@ const errorIndex = (props)=>{
**Requested access:** ${props.brew.accessType}

**Brew ID:** ${props.brew.brewId}`,

//Brew's cloning blocked
'10' : dedent`
## This brew's cloning features have been disabled

The author of this brew does not want other people using its contents, so viewing the source,
cloning the brew and downloading the text has been disabled.

If you think this is a mistake, you may contact the author.

If you are the author, please login to the account that has authorship of this brew.

:

**Brew ID:** ${props.brew.brewId}

**Brew Title:** ${props.brew.brewTitle}

**Brew Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}`,

// Brew locked by Administrators error
'100' : dedent`
Expand Down
41 changes: 23 additions & 18 deletions client/homebrew/pages/sharePage/sharePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,24 +87,29 @@ const SharePage = createClass({
</Nav.section>

<Nav.section>
{this.props.brew.shareId && <>
<PrintNavItem/>
<Nav.dropdown>
<Nav.item color='red' icon='fas fa-code'>
source
</Nav.item>
<Nav.item color='blue' icon='fas fa-eye' href={`/source/${this.processShareId()}`}>
view
</Nav.item>
{this.renderEditLink()}
<Nav.item color='blue' icon='fas fa-download' href={`/download/${this.processShareId()}`}>
download
</Nav.item>
<Nav.item color='blue' icon='fas fa-clone' href={`/new/${this.processShareId()}`}>
clone to new
</Nav.item>
</Nav.dropdown>
</>}
{this.props.brew.shareId && (
<>
<PrintNavItem />
{this.props.brew.cloning !== false && (
<Nav.dropdown>
<Nav.item color='red' icon='fas fa-code'>
source
</Nav.item>
<Nav.item color='blue' icon='fas fa-eye' href={`/source/${this.processShareId()}`}>
view
</Nav.item>
{this.renderEditLink()}
<Nav.item color='blue' icon='fas fa-download' href={`/download/${this.processShareId()}`}>
download
</Nav.item>
<Nav.item color='blue' icon='fas fa-clone' href={`/new/${this.processShareId()}`}>
clone to new
</Nav.item>
</Nav.dropdown>
)}
</>
)}

<RecentNavItem brew={this.props.brew} storageKey='view' />
<Account />
</Nav.section>
Expand Down
69 changes: 69 additions & 0 deletions client/homebrew/pages/sourcePage/sourcePage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import './sourcePage.less';
const UIPage = require('../basePages/uiPage/uiPage.jsx');

const SourcePage = (props) => {
const { brew } = props;

const sanitizeFilename = (filename) => {
return filename.replace(/[\/\\?%*:|"<>]/g, '_');
};

const prefix = 'HB - ';
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replace(/ /g, '');
if (!fileName || fileName.length === 0) {
fileName = `${prefix}-Untitled-Brew`;
}

// Function to encode text for HTML display
const encodeText = () => {
const replaceStrings = { '&': '&amp;', '<': '&lt;', '>': '&gt;' };
let text = brew.text || '';
for (const replaceStr in replaceStrings) {
text = text.replace(
new RegExp(replaceStr, 'g'),
replaceStrings[replaceStr]
);
}
return text;
};

// Function to handle downloading the text
const downloadText = () => {
const encodedText = encodeText();
const blob = new Blob([encodedText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${fileName}.txt`; // Set the filename for download
a.click(); // Trigger the download
URL.revokeObjectURL(url); // Clean up the URL object
};

const renderSourcePage = () => (
<div className="source">
<div className="buttons">
<button
className="copy"
onClick={() => {
navigator.clipboard.writeText(encodeText());
}}
>
Copy <i className="fas fa-copy" />
</button>
<button className="download" onClick={downloadText}>
Download <i className="fas fa-download" />
</button>
</div>

<code className="code">
<pre style={{ whiteSpace: 'pre' }}>{encodeText()}</pre>
</code>
</div>
);

// Render the page
return <UIPage brew={brew}>{renderSourcePage()}</UIPage>;
};

module.exports = SourcePage;
35 changes: 35 additions & 0 deletions client/homebrew/pages/sourcePage/sourcePage.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

.source {
position : relative;
width : 100%;
height : fit-content;
padding : 20px 100px 20px 20px;
background : #E8DECD;
border : 1px solid #A27A3E;
border-radius : 5px;

.buttons {
display:flex;
flex-direction: column;
position : absolute;
top : 0;
right : 0;
background : #00000024;
border-bottom : 1px solid #A27A3E;
border-left : 1px solid #A27A3E;

border-top-right-radius : 5px;
border-bottom-left-radius : 5px;

button {
color : black;
background : none;

&:has(+button) { border-bottom : 1px solid #A27A3E; }

&:hover { background : #00000024; }

&:active { background : #2722194C; }
}
}
}
57 changes: 45 additions & 12 deletions server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,21 +165,40 @@ app.get('/faq', async (req, res, next)=>{
});

//Source page
app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{
const { brew } = req;

const replaceStrings = { '&': '&amp;', '<': '&lt;', '>': '&gt;' };
let text = brew.text;
for (const replaceStr in replaceStrings) {
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
}
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
res.status(200).send(text);
app.get('/source/:id', asyncHandler(getBrew('share')), (req, res, next) => {
const { brew, account } = req;
const ownBrew = account && brew.authors.includes(account.username);

if (brew.cloning === false && !ownBrew) {
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
const error = new Error('Cloning blocked');
error.status = 401;
error.HBErrorCode = '10';
error.brewId = brew.shareId;
error.brewTitle = brew.title;
error.authors = brew.authors;
return next(error);
}
return next();
});

//Download brew source page
app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
const { brew } = req;
app.get('/download/:id', asyncHandler(getBrew('share')), (req, res, next) => {
const { brew, account } = req;
const ownBrew = account && brew.authors.includes(account.username);

if (brew.cloning === false && !ownBrew) {
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
const error = new Error('Cloning blocked');
error.status = 401;
error.HBErrorCode = '10';
error.brewId = brew.shareId;
error.brewTitle = brew.title;
error.authors = brew.authors;
return next(error);
}


sanitizeBrew(brew, 'share');
const prefix = 'HB - ';

Expand Down Expand Up @@ -290,7 +309,20 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res,

//New Page from ID
app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
const ownBrew = req.account && req.brew.authors.includes(req.account.username);
sanitizeBrew(req.brew, 'share');

if (req.brew.cloning === false && !ownBrew) {
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
const error = new Error('Cloning blocked');
error.status = 401;
error.HBErrorCode = '10';
error.brewId = req.brew.shareId;
error.brewTitle = req.brew.title;
error.authors = req.brew.authors;
return next(error);
}

splitTextStyleAndMetadata(req.brew);
const brew = {
shareId : req.brew.shareId,
Expand Down Expand Up @@ -407,6 +439,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
}));

const nodeEnv = config.get('node_env');

const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
// Local only
if(isLocalEnvironment){
Expand Down
4 changes: 3 additions & 1 deletion server/homebrew.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ const HomebrewSchema = mongoose.Schema({
updatedAt : { type: Date, default: Date.now },
lastViewed : { type: Date, default: Date.now },
views : { type: Number, default: 0 },
version : { type: Number, default: 1 }
version : { type: Number, default: 1 },

cloning : { type: Boolean, default:true}
}, { versionKey: false });

HomebrewSchema.statics.increaseView = async function(query) {
Expand Down