Removing vanilla encodeURIComponent, doing it manually for better control

This commit is contained in:
James Barnsley
2021-01-23 16:24:07 +13:00
parent 1bfc48e193
commit 4f561de7c5
22 changed files with 589 additions and 417 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import GridItem from './GridItem';
import { collate } from '../util/format';
import { collate, encodeUri } from '../util/format';
import * as uiActions from '../services/ui/actions';
import * as lastfmActions from '../services/lastfm/actions';
import * as spotifyActions from '../services/spotify/actions';
@ -47,7 +47,7 @@ class ArtistGrid extends React.Component {
type="artist"
item={artist}
show_source_icon={show_source_icon}
onClick={() => { history.push(`/artist/${encodeURIComponent(artist.uri)}`); }}
onClick={() => { history.push(`/artist/${encodeUri(artist.uri)}`); }}
lastfmActions={lastfmActions}
spotifyActions={spotifyActions}
spotifyAvailable={spotifyAvailable}

View File

@ -1,6 +1,7 @@
import React, { memo } from 'react';
import GridItem from './GridItem';
import { encodeUri } from '../util/format';
export default memo(({
categories,
@ -17,7 +18,7 @@ export default memo(({
key={category.uri}
type="category"
item={category}
link={`/discover/categories/${encodeURIComponent(category.uri)}`}
link={`/discover/categories/${encodeUri(category.uri)}`}
/>
))
}

View File

@ -12,6 +12,7 @@ import * as spotifyActions from '../../services/spotify/actions';
import { arrayOf } from '../../util/arrays';
import { decodeMopidyUri } from '../../util/helpers';
import { i18n } from '../../locale';
import { encodeUri } from '../../util/format';
class Dropzones extends React.Component {
constructor(props) {
@ -64,7 +65,7 @@ class Dropzones extends React.Component {
enqueueURIs(uris, from_uri, true);
break;
case 'add_to_playlist':
history.push(`/add-to-playlist/${encodeURIComponent(uris.join(','))}`);
history.push(`/add-to-playlist/${encodeUri(uris.join(','))}`);
// uris
break;
default:

View File

@ -5,6 +5,7 @@ import * as uiActions from '../../services/ui/actions';
import { uriType } from '../../util/helpers';
import Icon from '../Icon';
import { i18n } from '../../locale';
import { encodeUri } from '../../util/format';
class SearchForm extends React.Component {
constructor(props) {
@ -64,19 +65,19 @@ class SearchForm extends React.Component {
// check for uri type matching
switch (uriType(term)) {
case 'album':
history.push(`/album/${encodeURIComponent(term)}`);
history.push(`/album/${encodeUri(term)}`);
break;
case 'artist':
history.push(`/artist/${encodeURIComponent(term)}`);
history.push(`/artist/${encodeUri(term)}`);
break;
case 'playlist':
history.push(`/playlist/${encodeURIComponent(term)}`);
history.push(`/playlist/${encodeUri(term)}`);
break;
case 'track':
history.push(`/track/${encodeURIComponent(term)}`);
history.push(`/track/${encodeUri(term)}`);
break;
default:

View File

@ -10,6 +10,7 @@ import Icon from './Icon';
import Thumbnail from './Thumbnail';
import LinksSentence from './LinksSentence';
import { I18n } from '../locale';
import { encodeUri } from '../util/format';
class GridItem extends React.Component {
componentDidMount() {
@ -114,7 +115,7 @@ class GridItem extends React.Component {
if (!item) return null;
if (album) item = { ...item, ...album };
const link = customLink || `/${type}/${encodeURIComponent(item.uri)}`;
const link = customLink || `/${type}/${encodeUri(item.uri)}`;
return (
<Link

View File

@ -1,4 +1,3 @@
import React from 'react';
import { Link, useHistory, useLocation } from 'react-router-dom';
import { scrollTo } from '../util/helpers';
@ -43,7 +42,6 @@ export default ({
if (!retainScroll) scrollTo(scrollToProp, (scrollToProp));
};
// Decode both links. This handles issues where one link is encoded and the other isn't, but
// they're otherwise identical
const link = decodeURIComponent(to);

View File

@ -12,6 +12,7 @@ import {
scrollTo,
} from '../util/helpers';
import { I18n } from '../locale';
import { encodeUri } from '../util/format';
export default class ListItem extends React.Component {
componentDidMount() {
@ -48,7 +49,7 @@ export default class ListItem extends React.Component {
if (e.target.tagName.toLowerCase() !== 'a') {
e.preventDefault();
history.push(`${link_prefix}${encodeURIComponent(uri)}`);
history.push(`${link_prefix}${encodeUri(uri)}`);
scrollTo();
}
}
@ -64,7 +65,7 @@ export default class ListItem extends React.Component {
if (e.target.tagName.toLowerCase() !== 'a') {
e.preventDefault();
history.push(`${link_prefix}${encodeURIComponent(uri)}`);
history.push(`${link_prefix}${encodeUri(uri)}`);
scrollTo();
}
}

View File

@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as uiActions from '../services/ui/actions';
import GridItem from './GridItem';
import { encodeUri } from '../util/format';
class PlaylistGrid extends React.Component {
handleContextMenu(e, item) {
@ -35,7 +36,7 @@ class PlaylistGrid extends React.Component {
type="playlist"
item={playlist}
show_source_icon={this.props.show_source_icon}
onClick={(e) => { this.props.history.push(`/playlist/${encodeURIComponent(playlist.uri)}`); }}
onClick={(e) => { this.props.history.push(`/playlist/${encodeUri(playlist.uri)}`); }}
onContextMenu={(e) => this.handleContextMenu(e, playlist)}
/>
))

View File

@ -1,13 +1,14 @@
import React, { memo } from 'react';
import Link from './Link';
import { uriType as uriTypeHelper } from '../util/helpers';
import { encodeUri } from '../util/format';
export default memo((props) => {
let to = null;
let { uri, type } = props;
const uriType = type || uriTypeHelper(uri);
if (!props.unencoded) {
uri = encodeURIComponent(uri);
uri = encodeUri(uri);
}
switch (uriType) {

View File

@ -364,11 +364,12 @@ const CoreMiddleware = (function () {
break;
}
case 'LOAD_URI': {
const { uri, options } = action;
const source = uriSource(uri);
case 'LOAD_URIS': {
const { uris, options } = action;
uris.forEach((uri) => {
const fetch = () => {
const source = uriSource(uri);
// We need to pull type from the URI for Spotify as we use specific HTTP endpoints for
// each asset type, and their URIs facilitate this.
if (source === 'spotify') {
@ -379,12 +380,12 @@ const CoreMiddleware = (function () {
mopidyActions.getUris([uri], options);
}
};
ensureLoaded({
store,
action,
action: { uri, options },
fetch,
});
});
break;
}

View File

@ -1540,8 +1540,6 @@ const MopidyMiddleware = (function () {
(results) => (results.length ? formatTrack(results[0]) : null),
));
console.debug({ _response, tracks })
store.dispatch(coreActions.itemsLoaded(tracks));
store.dispatch(mopidyActions.getImages(arrayOf('uri', tracks)));

View File

@ -93,6 +93,8 @@ const removeDuplicates = function (array) {
const applyFilter = function (field, value, array, singular = false) {
const results = [];
if (!array || !array.length) return results;
for (let i = 0; i < array.length; i++) {
if (array[i][field] && String(array[i][field]).toLowerCase().includes(String(value).toLowerCase())) {
if (singular) {

View File

@ -282,9 +282,15 @@ const formatSimpleObjects = function (records = []) {
* '/' as this is a URL parameter delimiter
* @param {String} uri
*/
const encodeUri = (uri) => {
return encodeURIComponent(uri);
};
const encodeUri = (rawUri) => {
let uri = rawUri;
uri = uri.replace(/\//g, '%2F');
uri = uri.replace(/\?/g, '%3F');
return uri;
//encodeURIComponent(rawUri);
}
/**
* Rebuild a URI with some ugly-ass handling of encoding.
@ -301,6 +307,19 @@ const encodeUri = (uri) => {
* @param {String} rawUri
*/
const decodeUri = (rawUri) => {
let uri = rawUri;
uri = uri.replace(/ /g, '%20');
uri = uri.replace(/%2F/g, '/');
uri = uri.replace(/%3F/g, '?');
/**
* What if we removed encoding completely? It looks like mopidy URIs are encoded to be URL friendly,
* except for slashes. So in principle we could just replace slash with %2F but otherwise not use
* encode/decodeUriComponent.
*/
return uri;
/*
let uri = decodeURIComponent(rawUri);
uri = uri.replace(/,/g, '%2C');
uri = uri.replace(/'/g, '%27');
@ -310,10 +329,11 @@ const decodeUri = (rawUri) => {
uri = uri.replace(/\[/g, '%5B');
uri = uri.replace(/\]/g, '%5D');
uri = uri.replace(/ /g, '%20');
uri = uri.replace(/#/g, '%23');
//uri = uri.replace(/\//g, '%2F');
console.debug(uri);
return uri;
console.debug({ rawUri, uri });
return uri;*/
};
/**

View File

@ -1,4 +1,5 @@
import { indexToArray } from "./arrays";
import { encodeUri } from "./format";
/**
* Returns a function, that, as long as it continues to be invoked, will not
@ -339,8 +340,7 @@ const buildLink = (uri, type = null) => {
// Encode the whole URI as though it's a component. This makes it URL friendly for
// all Mopidy backends (some use URIs like local:track:http://rss.com/stuff.mp3) which
// is never going to work nicely.
link += encodeURIComponent(uri);
console.log({ type, link });
link += encodeUri(uri);
return link;
};

View File

@ -28,6 +28,7 @@ import Button from '../components/Button';
import { makeLoadingSelector, makeItemSelector } from '../util/selectors';
import { applyFilter, sortItems } from '../util/arrays';
import { trackEvent } from '../components/Trackable';
import { encodeUri } from '../util/format';
class Album extends React.Component {
constructor(props) {
@ -218,7 +219,7 @@ class Album extends React.Component {
return (
<ErrorMessage type="not-found" title="Not found">
<p>
{i18n('errors.uri_not_found', { uri: encodeURIComponent(uri) })}
{i18n('errors.uri_not_found', { uri })}
</p>
</ErrorMessage>
);

View File

@ -25,7 +25,7 @@ import {
sourceIcon,
titleCase,
} from '../util/helpers';
import { collate } from '../util/format';
import { collate, encodeUri } from '../util/format';
import { sortItems, applyFilter, arrayOf } from '../util/arrays';
import { i18n, I18n } from '../locale';
import Button from '../components/Button';
@ -265,7 +265,7 @@ class Artist extends React.Component {
/>
</div>
<Button
to={`/artist/${encodeURIComponent(uri)}/related-artists`}
to={`/artist/${encodeUri(uri)}/related-artists`}
scrollTo="#sub-views-menu"
>
<I18n path="artist.overview.related_artists.more" />
@ -492,7 +492,7 @@ class Artist extends React.Component {
return (
<ErrorMessage type="not-found" title="Not found">
<p>
<I18n path="errors.uri_not_found" uri={encodeURIComponent(uri)} />
<I18n path="errors.uri_not_found" uri={uri} />
</p>
</ErrorMessage>
);
@ -539,7 +539,7 @@ class Artist extends React.Component {
history={history}
activeClassName="sub-views__option--active"
className="sub-views__option"
to={`/artist/${encodeURIComponent(uri)}`}
to={`/artist/${encodeUri(uri)}`}
scrollTo="#sub-views-menu"
>
<h4><I18n path="artist.overview.title" /></h4>
@ -550,7 +550,7 @@ class Artist extends React.Component {
history={history}
activeClassName="sub-views__option--active"
className="sub-views__option"
to={`/artist/${encodeURIComponent(uri)}/tracks`}
to={`/artist/${encodeUri(uri)}/tracks`}
scrollTo="#sub-views-menu"
>
<h4><I18n path="artist.tracks.title" /></h4>
@ -562,7 +562,7 @@ class Artist extends React.Component {
history={history}
activeClassName="sub-views__option--active"
className="sub-views__option"
to={`/artist/${encodeURIComponent(uri)}/related-artists`}
to={`/artist/${encodeUri(uri)}/related-artists`}
scrollTo="#sub-views-menu"
>
<h4><I18n path="artist.related_artists.title" /></h4>
@ -573,7 +573,7 @@ class Artist extends React.Component {
history={history}
activeClassName="sub-views__option--active"
className="sub-views__option"
to={`/artist/${encodeURIComponent(uri)}/about`}
to={`/artist/${encodeUri(uri)}/about`}
scrollTo="#sub-views-menu"
>
<h4><I18n path="artist.about.title" /></h4>

View File

@ -30,6 +30,7 @@ import { trackEvent } from '../components/Trackable';
import { i18n, I18n } from '../locale';
import { makeItemSelector, makeLoadingSelector } from '../util/selectors';
import { sortItems, applyFilter } from '../util/arrays';
import { decodeUri, encodeUri } from '../util/format';
class Playlist extends React.Component {
constructor(props) {
@ -45,7 +46,7 @@ class Playlist extends React.Component {
// We accept the old format, and redirect to the new one
if (uri.includes('spotify:user:')) {
uri = uri.replace(/spotify:user:([^:]*?):/i, 'spotify:');
props.history.push(`/playlist/${encodeURIComponent(uri)}`);
props.history.push(`/playlist/${encodeUri(uri)}`);
}
}
@ -76,7 +77,7 @@ class Playlist extends React.Component {
} = this.props;
if (prevPlaylist && playlist && prevPlaylist.moved_to !== playlist.moved_to) {
push(`/playlist/${encodeURIComponent(playlist.moved_to)}`);
push(`/playlist/${encodeUri(playlist.moved_to)}`);
}
if (uri !== prevUri) {
@ -258,7 +259,7 @@ class Playlist extends React.Component {
<I18n path="actions.play" />
</Button>
<Button
to={`/playlist/${encodeURIComponent(uri)}/edit`}
to={`/playlist/${encodeUri(uri)}/edit`}
tracking={{ category: 'Playlist', action: 'Edit' }}
>
<I18n path="actions.edit" />
@ -280,7 +281,7 @@ class Playlist extends React.Component {
<I18n path="actions.play" />
</Button>
<Button
to={`/playlist/${encodeURIComponent(uri)}/edit`}
to={`/playlist/${encodeUri(uri)}/edit`}
tracking={{ category: 'Playlist', action: 'Edit' }}
>
<I18n path="actions.edit" />
@ -495,7 +496,7 @@ const mapStateToProps = (state, ownProps) => {
} = {},
} = state;
const uri = decodeMopidyUri(ownProps.match.params.uri);
const uri = decodeUri(ownProps.match.params.uri);
const playlistId = getFromUri('playlistid', uri);
const itemSelector = makeItemSelector(uri);
const loadingSelector = makeLoadingSelector([`(.*)${playlistId}(?!.*(following))(.*)`]);

View File

@ -18,15 +18,121 @@ import * as mopidyActions from '../services/mopidy/actions';
import * as spotifyActions from '../services/spotify/actions';
import * as lastfmActions from '../services/lastfm/actions';
import * as geniusActions from '../services/genius/actions';
import {
isLoading,
sourceIcon,
} from '../util/helpers';
import { sourceIcon } from '../util/helpers';
import { i18n, I18n } from '../locale';
import Button from '../components/Button';
import { makeLoadingSelector, makeItemSelector } from '../util/selectors';
import { decodeUri } from '../util/format';
const LyricsSelector = ({
track: {
uri,
lyrics_results,
lyrics_path,
} = {},
getTrackLyrics,
}) => {
if (lyrics_results === undefined || lyrics_results === null) return null;
if (lyrics_results.length <= 0) {
return (
<div className="field lyrics-selector">
<div className="input">
<input type="text" disabled="disabled" value="No results" />
<div className="description">
<I18n path="services.genius.switch_lyrics_result" />
</div>
</div>
</div>
);
}
return (
<div className="field lyrics-selector">
<div className="input">
<SelectField
onChange={(value) => getTrackLyrics(uri, value)}
options={
lyrics_results.map((result) => ({
value: result.path,
label: result.title,
defaultValue: (result.path === lyrics_path),
}))
}
/>
<div className="description">
<I18n path="services.genius.switch_lyrics_result" />
</div>
</div>
</div>
);
}
const LyricsContent = ({
authorized,
loading,
track: {
lyrics,
lyrics_path,
} = {},
}) => {
if (loading) return <Loader mini />;
if (!lyrics && !authorized) {
return (
<p className="no-results">
<I18n path="track.want_lyrics" />
<Link to="/settings/services/genius" scrollTo="#services-menu">
<I18n path="settings.title" />
</Link>
.
</p>
);
};
if (!lyrics) return null;
return (
<div className="lyrics">
<div className="content" dangerouslySetInnerHTML={{ __html: lyrics }} />
<div className="origin mid_grey-text">
<I18n path="track.lyrics_origin" />
<a
href={`https://genius.com${lyrics_path}`}
target="_blank"
rel="noreferrer noopener"
>
{`https://genius.com${lyrics_path}`}
</a>
</div>
</div>
);
}
const Lyrics = ({
loading,
authorized,
track,
getTrackLyrics,
}) => {
return (
<>
<h4>
<I18n path="track.lyrics" />
{loading && <Loader loading mini />}
</h4>
<LyricsSelector
getTrackLyrics={getTrackLyrics}
authorized={authorized}
track={track}
/>
<LyricsContent
track={track}
authorized={authorized}
/>
</>
);
}
class Track extends React.Component {
componentDidMount() {
const {
@ -85,12 +191,14 @@ class Track extends React.Component {
if (track) {
let artists = '';
if (artists) {
for (let i = 0; i < track.artists.length; i++) {
if (artists != '') {
artists += ', ';
}
artists += track.artists[i].name;
}
}
setWindowTitle(i18n('track.title_window', { name: track.name, artists }));
} else {
setWindowTitle(i18n('track.title'));
@ -121,80 +229,6 @@ class Track extends React.Component {
playURIs([uri], uri);
}
renderLyricsSelector = () => {
const {
track,
geniusActions: {
getTrackLyrics,
},
genius_authorized,
} = this.props;
if (!genius_authorized || track.lyrics_results === undefined || track.lyrics_results === null) {
return null;
} if (track.lyrics_results.length <= 0) {
return (
<div className="field lyrics-selector">
<div className="input">
<input type="text" disabled="disabled" value="No results" />
<div className="description">
<I18n path="services.genius.switch_lyrics_result" />
</div>
</div>
</div>
);
}
return (
<div className="field lyrics-selector">
<div className="input">
<SelectField
onChange={(value) => getTrackLyrics(track.uri, value)}
options={
track.lyrics_results.map((result) => ({
value: result.path,
label: result.title,
defaultValue: (result.path === track.lyrics_path),
}))
}
/>
<div className="description">
<I18n path="services.genius.switch_lyrics_result" />
</div>
</div>
</div>
);
}
renderLyrics = () => {
const {
track: {
lyrics,
lyrics_path,
} = {},
genius_authorized,
loadingLyrics,
} = this.props;
if (!lyrics || !genius_authorized || loadingLyrics) return null;
return (
<div className="lyrics">
<div className="content" dangerouslySetInnerHTML={{ __html: lyrics }} />
<div className="origin mid_grey-text">
<I18n path="track.lyrics_origin" />
<a
href={`https://genius.com${lyrics_path}`}
target="_blank"
rel="noreferrer noopener"
>
{`https://genius.com${lyrics_path}`}
</a>
</div>
</div>
);
}
render = () => {
const {
uri,
@ -203,6 +237,9 @@ class Track extends React.Component {
slim_mode,
uiActions,
genius_authorized,
geniusActions: {
getTrackLyrics
},
loadingLyrics,
} = this.props;
@ -288,22 +325,12 @@ class Track extends React.Component {
<ContextMenuTrigger onTrigger={this.handleContextMenu} />
</div>
<h4>
<I18n path="track.lyrics" />
{loadingLyrics && <Loader loading mini />}
</h4>
{!genius_authorized && (
<p className="no-results">
<I18n path="track.want_lyrics" />
<Link to="/settings/services/genius" scrollTo="#services-menu">
<I18n path="settings.title" />
</Link>
.
</p>
)}
{this.renderLyricsSelector()}
{this.renderLyrics()}
<Lyrics
loading={loadingLyrics}
authorized={genius_authorized}
getTrackLyrics={getTrackLyrics}
track={track}
/>
</div>
);

View File

@ -8,7 +8,7 @@ import Icon from '../../components/Icon';
import ErrorBoundary from '../../components/ErrorBoundary';
import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions';
import { formatImages } from '../../util/format';
import { formatImages, encodeUri } from '../../util/format';
import { I18n, i18n } from '../../locale';
class LibraryBrowse extends React.Component {
@ -91,7 +91,7 @@ class LibraryBrowse extends React.Component {
grid_items.push({
name: subdirectory.name,
link: `/library/browse/${encodeURIComponent(subdirectory.uri)}`,
link: `/library/browse/${encodeUri(subdirectory.uri)}`,
icons: formatImages(subdirectory.icons),
});
}

View File

@ -1,4 +1,3 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
@ -8,6 +7,7 @@ import List from '../../components/List';
import TrackList from '../../components/TrackList';
import GridItem from '../../components/GridItem';
import DropdownField from '../../components/Fields/DropdownField';
import FilterField from '../../components/Fields/FilterField';
import Icon from '../../components/Icon';
import URILink from '../../components/URILink';
import ErrorBoundary from '../../components/ErrorBoundary';
@ -18,9 +18,10 @@ import * as spotifyActions from '../../services/spotify/actions';
import {
isLoading,
} from '../../util/helpers';
import { arrayOf, sortItems } from '../../util/arrays';
import { arrayOf, sortItems, applyFilter } from '../../util/arrays';
import { i18n, I18n } from '../../locale';
import Button from '../../components/Button';
import { encodeUri } from '../../util/format';
class LibraryBrowseDirectory extends React.Component {
constructor(props) {
@ -153,7 +154,7 @@ class LibraryBrowseDirectory extends React.Component {
<GridItem
key={subdirectory.uri}
type="directory"
link={`/library/browse/${encodeURIComponent(subdirectory.uri)}`}
link={`/library/browse/${encodeUri(subdirectory.uri)}`}
item={subdirectory}
nocontext
/>
@ -172,6 +173,8 @@ class LibraryBrowseDirectory extends React.Component {
view,
} = this.props;
const {
filter,
per_page,
limit,
} = this.state;
@ -198,6 +201,9 @@ class LibraryBrowseDirectory extends React.Component {
let subdirectories = (directory.subdirectories && directory.subdirectories.length > 0 ? directory.subdirectories : null);
subdirectories = sortItems(subdirectories, 'name');
if (filter && filter !== '') {
subdirectories = applyFilter('name', filter, subdirectories);
}
const total_items = (directory.tracks ? directory.tracks.length : 0) + (subdirectories ? subdirectories.length : 0);
subdirectories = subdirectories.slice(0, limit);
@ -207,6 +213,9 @@ class LibraryBrowseDirectory extends React.Component {
if (limit_remaining > 0 && directory.tracks && directory.tracks.length) {
all_tracks = directory.tracks;
all_tracks = sortItems(all_tracks, 'name');
if (filter && filter !== '') {
tracks = applyFilter('name', filter, tracks);
}
tracks = all_tracks.slice(0, limit_remaining);
}
@ -223,6 +232,11 @@ class LibraryBrowseDirectory extends React.Component {
const options = (
<>
<FilterField
initialValue={filter}
handleChange={(value) => this.setState({ filter: value, limit: per_page })}
onSubmit={() => uiActions.hideContextMenu()}
/>
<DropdownField
icon="visibility"
name="View"