├── .dockerignore
├── docs
└── img
│ └── n.png
├── src
├── js
│ ├── adapter
│ │ ├── entermediadb
│ │ │ ├── template.html
│ │ │ └── index.js
│ │ ├── dummy
│ │ │ └── index.js
│ │ ├── github
│ │ │ └── index.js
│ │ └── googledrive
│ │ │ └── index.js
│ ├── shared
│ │ └── util
│ │ │ ├── uid.js
│ │ │ ├── createClass.js
│ │ │ └── messaging.js
│ ├── app
│ │ ├── model
│ │ │ ├── selection.js
│ │ │ ├── pick.js
│ │ │ └── item.js
│ │ ├── components
│ │ │ ├── storage
│ │ │ │ ├── template.html
│ │ │ │ └── index.js
│ │ │ ├── handle.js
│ │ │ ├── login
│ │ │ │ ├── index.js
│ │ │ │ └── template.html
│ │ │ ├── items
│ │ │ │ ├── grid.html
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── grid.js
│ │ │ └── tree
│ │ │ │ ├── tree.html
│ │ │ │ └── index.js
│ │ ├── config.js
│ │ ├── mixin
│ │ │ └── contextmenu.js
│ │ ├── util.js
│ │ ├── index.html
│ │ ├── locales.js
│ │ ├── index.js
│ │ └── adapter
│ │ │ └── base.js
│ └── picker
│ │ ├── components
│ │ ├── modal
│ │ │ ├── index.html
│ │ │ ├── index.css
│ │ │ └── index.js
│ │ └── ui
│ │ │ └── index.js
│ │ ├── util.js
│ │ └── index.js
├── sass
│ ├── _mixins.scss
│ ├── components
│ │ ├── _contextmenu.scss
│ │ ├── _sidebar.scss
│ │ ├── _loaders.scss
│ │ ├── _items.scss
│ │ ├── _tree.scss
│ │ ├── _navbar.scss
│ │ └── _items-grid.scss
│ ├── main.scss
│ └── picker-ui.scss
└── php
│ ├── AssetPicker.php
│ └── AssetPicker
│ └── Proxy.php
├── renovate.json
├── docker-compose.yml
├── dist
├── index.html
├── font
│ └── glyph.svg
└── js
│ ├── adapter
│ ├── dummy.js
│ ├── github.js
│ └── entermediadb.js
│ ├── maps
│ └── adapter
│ │ ├── dummy.js.map
│ │ └── github.js.map
│ └── picker.js
├── .gitignore
├── bower.json
├── composer.json
├── .snyk
├── Dockerfile
├── LICENSE
├── proxy.php
├── index.html
├── package.json
├── claudedocs
├── javascript-security-review.md
└── SECURITY_FIXES_SUMMARY.md
└── gulpfile.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | src
3 |
--------------------------------------------------------------------------------
/docs/img/n.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netresearch/assetpicker/HEAD/docs/img/n.png
--------------------------------------------------------------------------------
/src/js/adapter/entermediadb/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/js/shared/util/uid.js:
--------------------------------------------------------------------------------
1 | module.exports = function uid () {
2 | return '' + Math.random().toString(36).substr(2, 9);
3 | };
4 |
--------------------------------------------------------------------------------
/src/js/app/model/selection.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | storage: null,
3 | search: null,
4 | items: [],
5 | results: {}
6 | };
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/src/sass/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin clearfix {
2 | &:after {
3 | content: "";
4 | display: table;
5 | clear: both;
6 | }
7 | }
--------------------------------------------------------------------------------
/src/js/app/components/storage/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/js/picker/components/modal/index.html:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | web:
4 | image: nginx
5 | container_name: "assetpicker_web"
6 | ports:
7 | - 80:80
8 | volumes_from:
9 | - php
10 | links:
11 | - php
12 | php:
13 | build: .
14 | container_name: "assetpicker_php"
15 | volumes:
16 | - .:/var/www/html
17 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AssetPicker
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/js/shared/util/createClass.js:
--------------------------------------------------------------------------------
1 | module.exports = function(protoProps) {
2 | var result = function () {
3 | if (this.construct) {
4 | this.construct.apply(this, arguments);
5 | }
6 | };
7 |
8 | result.prototype = result;
9 | Object.keys(protoProps).forEach(function(key) {
10 | result.prototype[key] = protoProps[key];
11 | });
12 |
13 | return result;
14 | };
15 |
--------------------------------------------------------------------------------
/dist/font/glyph.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/sass/components/_contextmenu.scss:
--------------------------------------------------------------------------------
1 | #contextmenu {
2 | position: absolute;
3 | padding:3px 0;
4 | border-radius: 3px;
5 | background: #fff;
6 | box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.4);
7 | a, a:link, a:active, a:visited {
8 | display: block;
9 | padding:6px 16px;
10 | text-align: left;
11 | color: $gray;
12 | &:hover {
13 | text-decoration: none;
14 | background: $gray-lighter;
15 | }
16 | span {
17 | float: left;
18 | margin-right: 10px;
19 | position: relative;
20 | top:5px;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | .idea
23 | # Dependency directory
24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
25 | node_modules
26 |
27 | vendor
28 |
--------------------------------------------------------------------------------
/src/sass/components/_sidebar.scss:
--------------------------------------------------------------------------------
1 | #sidebar > div > div {
2 | background:$gray-lighter;
3 | padding:0 15px;
4 | .storage {
5 | margin: ($padding-large-vertical) 0;
6 | > li > .item {
7 | font-size: 13px;
8 | color: $gray;
9 | font-weight: 500;
10 | margin-bottom:6px;
11 | > .glyphicon {
12 | position: relative;
13 | left: -2px;
14 | margin-left: 2px;
15 | }
16 | }
17 | > li > .storage-filters {
18 | padding-left:6px;
19 | }
20 | }
21 | .panel {
22 | margin-top:1em;
23 | margin-left:-14px;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "assetpicker",
3 | "description": "A free asset or file picker with abstraction layer allowing several adapters like GitHub, EnterMediaDB, Amazon S3, Google Drive, Dropbox etc.",
4 | "keywords": [
5 | "js",
6 | "asset",
7 | "file",
8 | "picker",
9 | "selector",
10 | "github",
11 | "entermediadb"
12 | ],
13 | "homepage": "http://netresearch.github.io/assetpicker",
14 | "license": "MIT",
15 | "moduleType": "globals",
16 | "main": [
17 | "scss/bootstrap.scss",
18 | "dist/js/picker.min.js"
19 | ],
20 | "ignore": [
21 | "/.*",
22 | "docs"
23 | ],
24 | "dependencies": {
25 | }
26 | }
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "netresearch/assetpicker",
3 | "description": "A free asset or file picker with abstraction layer allowing several adapters like GitHub, EnterMediaDB, Amazon S3, Google Drive, Dropbox etc.",
4 | "type": "library",
5 | "authors": [
6 | {
7 | "name": "Christian Opitz",
8 | "email": "christian.opitz@netresearch.de"
9 | }
10 | ],
11 | "license": "MIT",
12 | "require": {
13 | "jenssegers/proxy": "^3.0"
14 | },
15 | "autoload": {
16 | "psr-4": {
17 | "Netresearch\\": "src/php"
18 | }
19 | },
20 | "config": {
21 | "platform": {
22 | "php": "7.4.33"
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/sass/components/_loaders.scss:
--------------------------------------------------------------------------------
1 | .loader-progress {
2 | height: 4px;
3 | width: 100%;
4 | position: relative;
5 | overflow: hidden;
6 | background-color: lighten($brand-primary, 15%);
7 | opacity: 0;
8 | transition: opacity 0.3s;
9 | }
10 | .loader-progress.active {
11 | opacity: 1;
12 | }
13 | .loader-progress.active:before{
14 | display: block;
15 | position: absolute;
16 | content: "";
17 | left: -200px;
18 | width: 200px;
19 | height: 4px;
20 | background-color: darken($brand-primary, 5%);
21 | animation: loader-progress 2s linear infinite;
22 | }
23 |
24 | @keyframes loader-progress {
25 | from {left: -200px; width: 0;}
26 | 50% {width: 30%;}
27 | 70% {width: 70%;}
28 | 80% { left: 50%;}
29 | 95% {left: 120%;}
30 | to {left: 100%;}
31 | }
--------------------------------------------------------------------------------
/.snyk:
--------------------------------------------------------------------------------
1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
2 | version: v1.25.0
3 | ignore: {}
4 | # patches apply the minimum changes required to fix a vulnerability
5 | patch:
6 | 'npm:debug:20170905':
7 | - gulp-livereload > mini-lr > body-parser > debug:
8 | patched: '2022-06-21T03:48:16.008Z'
9 | 'npm:minimatch:20160620':
10 | - gulp > vinyl-fs > glob-stream > minimatch:
11 | patched: '2022-06-21T03:48:16.008Z'
12 | - gulp > vinyl-fs > glob-watcher > gaze > globule > minimatch:
13 | patched: '2022-06-21T03:48:16.008Z'
14 | - gulp > vinyl-fs > glob-watcher > gaze > globule > glob > minimatch:
15 | patched: '2022-06-21T03:48:16.008Z'
16 | 'npm:ms:20170412':
17 | - gulp-livereload > mini-lr > body-parser > debug > ms:
18 | patched: '2022-06-21T03:48:16.008Z'
19 |
--------------------------------------------------------------------------------
/src/php/AssetPicker.php:
--------------------------------------------------------------------------------
1 |
10 | * @license http://www.netresearch.de Netresearch Copyright
11 | * @link http://www.netresearch.de
12 | */
13 |
14 | namespace Netresearch;
15 |
16 | /**
17 | * Class AssetPicker
18 | *
19 | * @category Netresearch
20 | * @package Netresearch
21 | * @author Christian Opitz
22 | * @license http://www.netresearch.de Netresearch Copyright
23 | * @link http://www.netresearch.de
24 | */
25 | class AssetPicker
26 | {
27 | /**
28 | * Get the path to the dist directory
29 | *
30 | * @return string
31 | */
32 | public static function getDistPath()
33 | {
34 | return __DIR__ . '/../../dist';
35 | }
36 | }
37 |
38 | ?>
39 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:8-fpm
2 |
3 | RUN curl -sS https://getcomposer.org/installer | php \
4 | && mv composer.phar /usr/local/bin/composer
5 |
6 | RUN mkdir -p /etc/nginx/conf.d
7 |
8 | RUN { \
9 | echo 'server {'; \
10 | echo ' listen 80;'; \
11 | echo ' index index.php index.html;'; \
12 | echo ' error_log /var/log/nginx/error.log;'; \
13 | echo ' access_log /var/log/nginx/access.log;'; \
14 | echo ' root /var/www/html;'; \
15 | echo ' location ~ \.php$ {'; \
16 | echo ' try_files $uri =404;'; \
17 | echo ' fastcgi_split_path_info ^(.+\.php)(/.+)$;'; \
18 | echo ' fastcgi_pass php:9000;'; \
19 | echo ' fastcgi_index index.php;'; \
20 | echo ' include fastcgi_params;'; \
21 | echo ' fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;'; \
22 | echo ' fastcgi_param PATH_INFO $fastcgi_path_info;'; \
23 | echo ' }'; \
24 | echo '}'; \
25 | } | tee /etc/nginx/conf.d/default.conf
26 |
27 | VOLUME /etc/nginx/conf.d
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Alex Sears
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/src/js/app/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | title: 'AssetPicker',
3 | storages: {
4 | /* entermediaDB: {
5 | adapter: 'entermediadb',
6 | url: 'http://em9.entermediadb.org/openinstitute',
7 | proxy: true
8 | },
9 | github: {
10 | adapter: 'github',
11 | username: 'netresearch',
12 | repository: 'assetpicker'
13 | } */
14 | },
15 | proxy: {
16 | url: 'proxy.php?to={{url}}',
17 | all: false
18 | },
19 | github: {
20 | //tokenBla: 'j2332dwedcdj33dx3jm8389xdq'
21 | },
22 | pick: {
23 | limit: 1,
24 | types: ['file'],
25 | extensions: []
26 | },
27 | language: 'auto',
28 | debug: false,
29 | adapters: {
30 | github: 'adapter/github.js',
31 | entermediadb: 'adapter/entermediadb.js',
32 | googledrive: 'adapter/googledrive.js',
33 | dummy: 'adapter/dummy.js'
34 | },
35 | // Way to deliver thumbnails in picked assets:
36 | // 'url' for the url to the thumbnail
37 | // 'data' for the image data uri
38 | thumbnails: 'url'
39 | };
40 |
--------------------------------------------------------------------------------
/src/js/picker/util.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | addClass: function (element, className) {
3 | if (element.className) {
4 | if (element.className.split(' ').indexOf(className) === -1) {
5 | element.className += ' ' + className;
6 | }
7 | } else {
8 | element.className = className;
9 | }
10 | },
11 | removeClass: function (element, className) {
12 | if (element.className) {
13 | var classNames = element.className.split(' '), newClassNames = [];
14 | for (var i = 0, l = classNames.length; i < l; i++) {
15 | if (classNames[i] !== className) {
16 | newClassNames.push(classNames[i]);
17 | }
18 | }
19 | element.className = newClassNames.join(' ');
20 | }
21 | },
22 | loadCss: function (file) {
23 | var link = document.createElement("link");
24 | link.href = file;
25 | link.type = "text/css";
26 | link.rel = "stylesheet";
27 | link.media = "screen,print";
28 |
29 | document.getElementsByTagName("head")[0].appendChild(link);
30 | }
31 | };
--------------------------------------------------------------------------------
/src/sass/components/_items.scss:
--------------------------------------------------------------------------------
1 | .items-container {
2 | @import "items-grid";
3 | .feedback {
4 | position: absolute;
5 | top:50%;
6 | margin-top:-10px;
7 | width:100%;
8 | text-align: center;
9 | color:$gray-light;
10 | }
11 | .storage {
12 | &:first-child {
13 | margin-top:15px;
14 | }
15 | position: relative;
16 | padding:15px;
17 | padding-left: 72px;
18 | .glyphicon {
19 | font-size:32px;
20 | position: absolute;
21 | left: 22px;
22 | top: 15px;
23 | }
24 | h6 {
25 | margin: -2px 0 2px;
26 | }
27 | p {
28 | margin: 0;
29 | }
30 | h6, p {
31 | line-height: normal;
32 | white-space: nowrap;
33 | overflow: hidden;
34 | text-overflow: ellipsis; /** IE6+, Firefox 7+, Opera 11+, Chrome, Safari **/
35 | -o-text-overflow: ellipsis;
36 | }
37 | cursor: default;
38 | &:hover, &.selected {
39 | background: $gray-lighter;
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/js/app/components/handle.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | template: '',
3 | data: function () {
4 | return {
5 | x: undefined
6 | };
7 | },
8 | ready: function () {
9 | document.body.addEventListener('mousemove', this.drag);
10 | var parent = this.$el.parentNode, defaultCursor = parent.style.cursor;
11 | this.$el.addEventListener('mousedown', function (e) {
12 | parent.style.cursor = 'col-resize';
13 | var drag = function (e) {
14 | this.x = e.pageX;
15 | this.$dispatch('handle-move');
16 | }.bind(this);
17 | var leave = function () {
18 | parent.style.cursor = defaultCursor;
19 | parent.removeEventListener('mousemove', drag);
20 | document.body.removeEventListener('mouseleave', leave);
21 | document.body.removeEventListener('mouseup', leave);
22 | }.bind(this);
23 | parent.addEventListener('mousemove', drag);
24 | document.body.addEventListener('mouseleave', leave);
25 | document.body.addEventListener('mouseup', leave);
26 | }.bind(this));
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/src/js/app/components/login/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | template: require('./template.html'),
3 | props: {
4 | failure: Boolean,
5 | username: String,
6 | hint: String
7 | },
8 | data: function() {
9 | return {
10 | password: null
11 | }
12 | },
13 | methods: {
14 | submit: function (username, password) {
15 | this.$dispatch('login-submit', username, password);
16 | },
17 | login: function (authenticate) {
18 | return this.$promise(function (resolve) {
19 | var root = this.$root;
20 | root.isLogin = true;
21 | this.$on('login-submit', function (username, password) {
22 | authenticate(username, password, function (result) {
23 | if (result) {
24 | this.$remove().$destroy();
25 | root.isLogin = false;
26 | resolve();
27 | } else {
28 | this.failure = true;
29 | }
30 | }.bind(this));
31 | }.bind(this));
32 | });
33 | }
34 | }
35 | };
--------------------------------------------------------------------------------
/src/js/app/components/items/grid.html:
--------------------------------------------------------------------------------
1 |
2 |
-
3 |
4 |
5 |
10 |
11 |
12 |
![]()
13 | {{item.name}}
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/js/app/components/login/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{t('login.login')}}
4 |
5 |
6 |
20 |
21 |
--------------------------------------------------------------------------------
/proxy.php:
--------------------------------------------------------------------------------
1 |
9 | * @license http://www.netresearch.de Netresearch Copyright
10 | * @link http://www.netresearch.de
11 | */
12 |
13 | function includeIfExists($file) {
14 | if (file_exists($file)) {
15 | return include $file;
16 | }
17 | }
18 | if ((!$loader = includeIfExists(__DIR__ . '/vendor/autoload.php')) && (!$loader = includeIfExists(__DIR__.'/../../autoload.php'))) {
19 | die('You must set up the project dependencies, run the following commands:'.PHP_EOL.
20 | 'curl -s http://getcomposer.org/installer | php'.PHP_EOL.
21 | 'php composer.phar install'.PHP_EOL);
22 | }
23 |
24 | $request = \Symfony\Component\HttpFoundation\Request::createFromGlobals();
25 |
26 | try {
27 | if ($request->query->has('to')) {
28 | $proxyTo = $request->query->get('to');
29 | $request->query->remove('to');
30 | $proxy = new \Netresearch\AssetPicker\Proxy();
31 | $proxy->forward($request)->to($proxyTo)->send();
32 | } else {
33 | throw new Exception('No target provided');
34 | }
35 | } catch (\Exception $e) {
36 | header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500);
37 | echo $e;
38 | }
39 |
--------------------------------------------------------------------------------
/src/sass/components/_tree.scss:
--------------------------------------------------------------------------------
1 | ul.tree {
2 | li {
3 | position: relative;
4 | padding-left: 13px;
5 | > .glyphicon {
6 | position: absolute;
7 | left:0px;
8 | top: 7px;
9 | z-index:2;
10 | font-size: 9px;
11 | margin-right: 2px;
12 | cursor: default;
13 | &:hover {
14 | color: #000;
15 | }
16 | }
17 | .item {
18 | z-index:1;
19 | white-space: nowrap;
20 | text-overflow: ellipsis; /** IE6+, Firefox 7+, Opera 11+, Chrome, Safari **/
21 | -o-text-overflow: ellipsis; /** Opera 9 & 10 **/
22 | cursor: pointer;
23 | &:before {
24 | content: "A";
25 | display: block;
26 | position: absolute;
27 | left: -1000px;
28 | width:2000px;
29 | right: 0;
30 | background: #000;
31 | opacity: 0;
32 | top:0;
33 | }
34 | &.selected:before {
35 | opacity: 0.15;
36 | }
37 | &:hover:before {
38 | opacity: 0.07;
39 | }
40 | }
41 | }
42 | &, ul {
43 | list-style-type: none;
44 | padding:0;
45 | margin:0;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/js/app/components/tree/tree.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 | {{entryPoint}}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {{item.name}}
21 |
22 |
23 |
30 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AssetPicker Development
6 |
7 |
8 |
9 |
10 |
11 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "assetpicker",
3 | "version": "1.3.4",
4 | "description": "A free asset or file picker with abstraction layer allowing several adapters like GitHub, EnterMediaDB, Amazon S3, Google Drive, Dropbox etc.",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/netresearch/assetpicker.git"
8 | },
9 | "author": "Christian Opitz",
10 | "license": "MIT",
11 | "bugs": {
12 | "url": "https://github.com/netresearch/assetpicker/issues"
13 | },
14 | "homepage": "https://github.com/netresearch/assetpicker#readme",
15 | "main": "src/js/picker/index.js",
16 | "directories": {
17 | "lib": "src/js"
18 | },
19 | "dependencies": {
20 | "escape-string-regexp": "^5.0.0",
21 | "extend": "^3.0.0",
22 | "fecha": "^4.2.3",
23 | "insert-css": "^2.0.0",
24 | "partialify": "^3.1.5",
25 | "vue": "^3.2.30",
26 | "vue-i18n-mixin": "^0.1.0",
27 | "vue-infinite-scroll": "^2.0.0",
28 | "vue-resource": "^1.5.3",
29 | "@snyk/protect": "latest"
30 | },
31 | "devDependencies": {
32 | "browserify": "^17.0.0",
33 | "core-js": "^3.22.7",
34 | "eslint": "^9.0.0",
35 | "gulp": "^5.0.0",
36 | "gulp-concat": "^2.6.0",
37 | "gulp-livereload": "^4.0.2",
38 | "gulp-sass": "^6.0.0",
39 | "gulp-sourcemaps": "^3.0.0",
40 | "gulp-uglify": "^3.0.0",
41 | "jsdom": "^27.0.0",
42 | "vinyl-buffer": "^1.0.0",
43 | "vinyl-source-stream": "^2.0.0"
44 | },
45 | "browserify": {
46 | "transform": [
47 | "partialify"
48 | ]
49 | },
50 | "scripts": {
51 | "prepublish": "npm run snyk-protect",
52 | "snyk-protect": "snyk-protect"
53 | },
54 | "snyk": true
55 | }
56 |
--------------------------------------------------------------------------------
/dist/js/adapter/dummy.js:
--------------------------------------------------------------------------------
1 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.AssetPickerAdapterDummy=t()}}(function(){return function t(e,i,n){function o(s,u){if(!i[s]){if(!e[s]){var f="function"==typeof require&&require;if(!u&&f)return f(s,!0);if(r)return r(s,!0);var a=new Error("Cannot find module '"+s+"'");throw a.code="MODULE_NOT_FOUND",a}var d=i[s]={exports:{}};e[s][0].call(d.exports,function(t){var i=e[s][1][t];return o(i?i:t)},d,d.exports,t,e,i,n)}return i[s].exports}for(var r="function"==typeof require&&require,s=0;s=1400||this.mockLoad(function(){t.items=this.createItems()})},"load-more-items":function(t){this.lastId>=1400||this.mockLoad(function(){this.createItems().forEach(function(e){t.push(e)})})}},methods:{mockLoad:function(t){this.$root.loading++,window.setTimeout(function(){t.call(this),this.$root.loading--}.bind(this),500)},item:function(t,e){return this.createItem({id:""+this.lastId++,type:t?"file":"dir",extension:t,name:"Random "+(t||" directory")+(e?" with thumb":"")+" "+this.lastId,thumbnail:e})},createItems:function(){var t=[],e=["txt","pdf","xls","doc","pot","jpeg","zip","mp3","avi","html","any"];t.push(this.item());for(var i=0,n=e.length;i
10 | * @license http://www.netresearch.de Netresearch Copyright
11 | * @link http://www.netresearch.de
12 | */
13 |
14 | namespace Netresearch\AssetPicker;
15 | use Proxy\Adapter\Guzzle\GuzzleAdapter;
16 |
17 | /**
18 | * Class Proxy
19 | *
20 | * @category Netresearch
21 | * @package Netresearch\AssetPicker
22 | * @author Christian Opitz
23 | * @license http://www.netresearch.de Netresearch Copyright
24 | * @link http://www.netresearch.de
25 | */
26 | class Proxy extends \Proxy\Proxy
27 | {
28 | /**
29 | * Proxy constructor.
30 | */
31 | public function __construct()
32 | {
33 | parent::__construct(new GuzzleAdapter());
34 | $this->addResponseFilter(function(\Symfony\Component\HttpFoundation\Response $response) {
35 | $response->prepare($this->request);
36 | if ($response->headers->has('transfer-encoding')) {
37 | $response->headers->remove('transfer-encoding');
38 | }
39 | if ($response->isRedirect()) {
40 | $baseUrl = $this->request->getBaseUrl();
41 | if (basename($baseUrl) !== basename($this->request->getScriptName())) {
42 | $baseUrl .= $this->request->getPathInfo();
43 | }
44 | $response->headers->set(
45 | 'location',
46 | $this->request->getSchemeAndHttpHost() . $baseUrl . '?to='
47 | . urlencode($response->headers->get('location'))
48 | );
49 | }
50 | });
51 | }
52 | }
53 |
54 | ?>
55 |
--------------------------------------------------------------------------------
/src/js/app/model/pick.js:
--------------------------------------------------------------------------------
1 | var pick = [], candidate;
2 | var config = require('../config');
3 |
4 | pick.isAllowed = function (item) {
5 | var conf = config.pick;
6 | return (!conf.types || !conf.types.length || conf.types.indexOf(item.type)) > -1 && (!conf.extensions || !conf.extensions.length || conf.extensions.indexOf(item.extension) > -1);
7 | };
8 | pick.contains = function (item) {
9 | for (var i = 0, l = pick.length; i < l; i++) {
10 | if (pick[i].id === item.id && pick[i].storage === item.storage) {
11 | return true;
12 | }
13 | }
14 | };
15 | pick.candidate = function (item) {
16 | if (item) {
17 | this.add(item);
18 | }
19 | candidate = item;
20 | };
21 | pick.toggle = function (item) {
22 | if (this.contains(item)) {
23 | this.remove(item);
24 | } else {
25 | this.add(item);
26 | }
27 | };
28 | pick.add = function (item) {
29 | if (!this.contains(item) && this.isAllowed(item)) {
30 | if (candidate && item !== candidate && this.contains(candidate)) {
31 | this.remove(candidate);
32 | }
33 | while (config.pick.limit && this.length >= config.pick.limit) {
34 | this.shift();
35 | }
36 | this.push(item);
37 | }
38 | };
39 | pick.remove = function (item) {
40 | for (var i = 0, l = this.length; i < l; i++) {
41 | var next = this.shift();
42 | if (next.id !== item.id || next.storage !== item.storage) {
43 | pick.push(next);
44 | }
45 | }
46 | if (!pick.length && candidate && item !== candidate && this.isAllowed(candidate)) {
47 | this.push(candidate);
48 | }
49 | };
50 | pick.clear = function () {
51 | while (this.length) {
52 | this.pop();
53 | }
54 | };
55 | pick.export = function () {
56 | return config.pick.limit === 1 ? this[0] : this.slice(0);
57 | };
58 |
59 | module.exports = pick;
60 |
--------------------------------------------------------------------------------
/src/js/app/components/items/index.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
{{t('stage.nothingFound')}}
8 |
9 |
10 | {{storages[storage].label || storage}}
11 |
12 | Show all...
13 |
14 |
15 |
16 |
17 |
18 | {{storages[storage].label || storage}}
19 | {{t('stage.noItems')}}
20 |
21 |
22 |
23 |
24 |
25 |
{{config.label || storage}}
26 |
{{$root.loaded ? $interpolate(t(config.adapter + '.description')) : ''}}
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/js/picker/components/modal/index.css:
--------------------------------------------------------------------------------
1 | .assetpicker-modal {
2 | position: fixed;
3 | top: 0;
4 | bottom: 0;
5 | left: 0;
6 | right: 0;
7 | background: rgba(0, 0, 0, 0);
8 | z-index: -1;
9 | transition: background 0.2s, z-index 0s 0.2s;
10 | }
11 | .assetpicker-modal.assetpicker-modal-open {
12 | z-index:3000;
13 | background: rgba(0, 0, 0, 0.8);
14 | transition: background 0.2s
15 | }
16 | .assetpicker-modal .assetpicker-modal-inner {
17 | position: absolute;
18 | background: #fff;
19 | width: 800px;
20 | height: 500px;
21 | max-width: calc(100% - 30px);
22 | max-height: calc(100% - 30px);
23 | left:50%;
24 | top:50%;
25 | transform: translateY(-50%) translateX(-50%);
26 | opacity: 0;
27 | transition: opacity 0.1s 0.1s;
28 | border-radius: 4px;
29 | overflow: hidden;
30 | }
31 | .assetpicker-modal.assetpicker-maximized .assetpicker-modal-inner {
32 | width: 100%;
33 | height: 100%;
34 | }
35 | .assetpicker-modal.assetpicker-modal-open .assetpicker-modal-inner {
36 | opacity: 1;
37 | }
38 | .assetpicker-modal .assetpicker-modal-inner iframe {
39 | position: absolute;
40 | background: transparent;
41 | top:0;
42 | left:0;
43 | width:100%;
44 | height: 100%;
45 | overflow: hidden;
46 | border: none;
47 | }
48 |
49 | .assetpicker-loader {
50 | border: 5px solid gray;
51 | border-radius: 30px;
52 | height: 30px;
53 | left: 50%;
54 | margin: -15px 0 0 -15px;
55 | opacity: 0;
56 | position: absolute;
57 | top: 50%;
58 | width: 30px;
59 |
60 | animation: assetpicker-loader-pulsate 1s ease-out;
61 | animation-iteration-count: infinite;
62 | }
63 | .assetpicker-ready .assetpicker-loader {
64 | display: none;
65 | animation: none;
66 | }
67 |
68 | @keyframes assetpicker-loader-pulsate {
69 | 0% {
70 | transform: scale(.1);
71 | opacity: 0.0;
72 | }
73 | 50% {
74 | opacity: 1;
75 | }
76 | 100% {
77 | transform: scale(1.2);
78 | opacity: 0;
79 | }
80 | }
--------------------------------------------------------------------------------
/src/js/app/components/storage/index.js:
--------------------------------------------------------------------------------
1 | var Vue = require('vue');
2 |
3 | module.exports = {
4 | template: require('./template.html'),
5 | props: {
6 | storage: Object,
7 | open: Boolean,
8 | id: String
9 | },
10 | data: function () {
11 | return {
12 | items: null,
13 | selection: require('../../model/selection'),
14 | pick: require('../../model/pick'),
15 | fetch: false
16 | };
17 | },
18 | ready: function () {
19 | if (this.open) {
20 | this.$children[0].select();
21 | }
22 | },
23 | events: {
24 | 'select-item': function(item) {
25 | this.selection.search = null;
26 | if (item instanceof Vue) {
27 | // Triggered from sidebar
28 | item.items.storage = this.id;
29 | this.selection.items = item.items;
30 | this.pick.candidate(item.item);
31 | } else {
32 | // Triggered from stage
33 | if (item.storage === this.id) {
34 | this.open = true;
35 | this.$nextTick(function () {
36 | this.$broadcast('select-item', item);
37 | });
38 | } else {
39 | this.$broadcast('deselect-items');
40 | }
41 | }
42 | },
43 | 'load-more-items': function (results) {
44 | if (results.storage === this.id) {
45 | this.$broadcast('load-more-items', results);
46 | }
47 | }
48 | },
49 | watch: {
50 | 'selection.search': function (sword) {
51 | if (sword) {
52 | this.pick.candidate(null);
53 | this.$broadcast('deselect-items');
54 | this.$set('selection.results.' + this.id, []);
55 | var results = this.$get('selection.results.' + this.id);
56 | results.storage = this.id;
57 | this.fetch = true;
58 | this.$nextTick(function() {
59 | this.$broadcast('search', sword, results);
60 | });
61 | }
62 | }
63 | },
64 | components: {
65 | }
66 | };
67 |
--------------------------------------------------------------------------------
/src/js/adapter/dummy/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | translations: {
3 | description: {
4 | en: 'Dummy adapter',
5 | de: 'Dummy-Adapter'
6 | }
7 | },
8 | data: function() {
9 | return {
10 | lastId: 1
11 | }
12 | },
13 | events: {
14 | 'load-items': function (tree) {
15 | if (this.lastId >= 1400) {
16 | return;
17 | }
18 | this.mockLoad(function() {
19 | tree.items = this.createItems();
20 | });
21 | },
22 | 'load-more-items': function (items) {
23 | if (this.lastId >= 1400) {
24 | return;
25 | }
26 | this.mockLoad(function() {
27 | this.createItems().forEach(function(item) {
28 | items.push(item);
29 | });
30 | });
31 | }
32 | },
33 | methods: {
34 | mockLoad: function(callback) {
35 | this.$root.loading++;
36 | window.setTimeout(function() {
37 | callback.call(this);
38 | this.$root.loading--;
39 | }.bind(this), 500);
40 | },
41 | item: function(extension, thumbnail) {
42 | return this.createItem({
43 | id: '' + (this.lastId++),
44 | type: extension ? 'file' : 'dir',
45 | extension: extension,
46 | name: 'Random ' + (extension || ' directory') + (thumbnail ? ' with thumb' : '') + ' ' + this.lastId,
47 | thumbnail: thumbnail
48 | });
49 | },
50 | createItems: function () {
51 | var items = [],
52 | extensions = ['txt', 'pdf', 'xls', 'doc', 'pot', 'jpeg', 'zip', 'mp3', 'avi', 'html', 'any'];
53 | items.push(this.item());
54 | for (var i = 0, l = extensions.length; i < l; i++) {
55 | items.push(this.item(extensions[i]));
56 | }
57 | items.push(this.item('jpeg', 'http://lorempixel.com/nature/160/200'));
58 | items.push(this.item('jpeg', 'http://lorempixel.com/nature/200/160'));
59 | items.total = 10 * items.length;
60 | return items;
61 | }
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/src/js/app/mixin/contextmenu.js:
--------------------------------------------------------------------------------
1 | var menu, currentItem, bodyListener = function() {
2 | if (menu) {
3 | menu.parentNode.removeChild(menu);
4 | menu = undefined;
5 | }
6 | if (currentItem) {
7 | currentItem.className = currentItem.className.replace(/(^| )contextmenu( |$)/, '$2');
8 | currentItem = undefined;
9 | }
10 | };
11 | document.body.addEventListener('click', bodyListener);
12 | window.addEventListener('blur', bodyListener);
13 | document.body.addEventListener('contextmenu', function(e) {
14 | e.preventDefault();
15 | bodyListener();
16 | });
17 |
18 | module.exports = {
19 | created: function() {
20 | if (!this.$options.contextmenu) {
21 | throw 'Your component needs to have an option contextmenu';
22 | }
23 | if (typeof this.$options.contextmenu !== 'function') {
24 | throw 'contextmenu option must be a function returning an array';
25 | }
26 | },
27 | methods: {
28 | contextmenu: function(e) {
29 | bodyListener();
30 | e.preventDefault();
31 | e.stopPropagation();
32 | e.stopImmediatePropagation();
33 | var items = this.$options.contextmenu.call(this);
34 | if (!items.length) {
35 | return;
36 | }
37 | currentItem = this.$el;
38 | currentItem.className = (currentItem.className ? currentItem.className + ' ' : '') + 'contextmenu';
39 | menu = this.$root.$el.appendChild(document.createElement('div'));
40 | menu.id = 'contextmenu';
41 | items.forEach(function(item) {
42 | var a = menu.appendChild(document.createElement('a'));
43 | a.innerHTML = item.label;
44 | if (item.link) {
45 | a.setAttribute('href', item.link);
46 | a.setAttribute('target', '_blank');
47 | } else if (item.click) {
48 | a.addEventListener('click', function(e) {
49 | e.preventDefault();
50 | item.click.call(this);
51 | }.bind(this));
52 | }
53 | }.bind(this));
54 |
55 | var largestWidth = window.innerWidth - menu.offsetWidth - 10,
56 | largestHeight = window.innerHeight - menu.offsetHeight - 10;
57 |
58 | menu.style.left = Math.min(e.x, largestWidth) + 'px';
59 | menu.style.top = Math.min(e.y, largestHeight) + 'px';
60 | }
61 | }
62 | };
63 |
--------------------------------------------------------------------------------
/dist/js/adapter/github.js:
--------------------------------------------------------------------------------
1 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.AssetPickerAdapterGithub=t()}}(function(){return function t(e,n,i){function o(s,a){if(!n[s]){if(!e[s]){var u="function"==typeof require&&require;if(!a&&u)return u(s,!0);if(r)return r(s,!0);var h=new Error("Cannot find module '"+s+"'");throw h.code="MODULE_NOT_FOUND",h}var f=n[s]={exports:{}};e[s][0].call(f.exports,function(t){var n=e[s][1][t];return o(n?n:t)},f,f.exports,t,e,n,i)}return n[s].exports}for(var r="function"==typeof require&&require,s=0;sli>a {
10 | padding-top: $padding;
11 | padding-bottom: $padding;
12 | }
13 | .navbar-form {
14 | margin-top: $padding - 4;
15 | margin-bottom: $padding - 4;
16 | }
17 | .nav {
18 | font-size: $navbar-font-size;
19 | }
20 | .loader-progress {
21 | position: absolute;
22 | bottom: 0;
23 | margin:0;
24 | }
25 | .navbar-window-icons {
26 | margin:0 0 0 5px;
27 | > li {
28 | margin-left:10px;
29 | }
30 | a {
31 | cursor: pointer;
32 | display:inline-block;
33 | width:18px;
34 | height:24px;
35 | padding: 0;
36 | border-color: lighten($brand-primary, 30%);
37 | &:before {
38 | border-color: lighten($brand-primary, 30%);
39 | }
40 | &:hover {
41 | border-color: #fff;
42 | }
43 | &:hover:before {
44 | border-color: #fff;
45 | }
46 | }
47 | .close-x {
48 | font-size: 32px;
49 | position: relative;
50 | top: -2px;
51 | }
52 | .minmax {
53 | $border-width: 2px;
54 | $max-size: 16px;
55 | $min-size: 12px;
56 | position: relative;
57 | top: $border-width;
58 | display: inline-block;
59 | width: $max-size;
60 | height: $max-size;
61 | border: $border-width solid;
62 | background: $brand-primary;
63 | border-radius: $border-width;
64 | transition: border-color 0.2s;
65 | &.minimize {
66 | top: $border-width / 2;
67 | margin-right: $min-size / 2 - $border-width;
68 | width: $min-size;
69 | height: $min-size;
70 | &:before {
71 | z-index: -1;
72 | content: " ";
73 | display: block;
74 | position: absolute;
75 | border: $border-width solid;
76 | background: $brand-primary;
77 | border-radius: $border-width;
78 | width: $min-size;
79 | height: $min-size;
80 | right: $min-size / -2;
81 | top: $min-size / -2 + $border-width / 2;
82 | }
83 | }
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/src/js/app/components/items/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | template: require('./index.html'),
3 | directives: {infiniteScroll: require('vue-infinite-scroll').infiniteScroll},
4 | props: {
5 | layout: {
6 | type: String,
7 | default: 'grid'
8 | }
9 | },
10 | data: function () {
11 | var selection = require('../../model/selection');
12 | var config = require('../../config');
13 | var keys = Object.keys(config.storages);
14 | return {
15 | selection: selection,
16 | storage: keys.length > 1 ? null : keys[0],
17 | config: config,
18 | picked: require('../../model/pick')
19 | }
20 | },
21 | calculated: {
22 | numStorages: function () {
23 | return Object.keys(this.config.storages).length;
24 | }
25 | },
26 | events: {
27 | 'select-item': function (item) {
28 | this.storage = item.storage;
29 | return true;
30 | },
31 | 'resize': function() {
32 | this.invalidateLayout();
33 | return true;
34 | }
35 | },
36 | watch: {
37 | 'selection.search': function (sword) {
38 | this.$nextTick(function () {
39 | if (sword) {
40 | this.storage = undefined;
41 | }
42 | });
43 | },
44 | 'selection.items': function (items) {
45 | this.storage = items.storage;
46 | this.$nextTick(this.invalidateLayout);
47 | },
48 | 'config.storages': function (storages) {
49 | var keys = Object.keys(storages);
50 | if (this.selection.search || this.storage && keys.indexOf(this.storage) === -1) {
51 | this.$nextTick(function () {
52 | this.$root.$broadcast('select-item', {storage: keys.length === 1 ? keys[0] : undefined})
53 | });
54 | }
55 | }
56 | },
57 | methods: {
58 | invalidateLayout: function() {
59 | var items = this.selection.items;
60 | if ((!this.selection.search || this.storage) && items && !items.loading && items.total && items.length < items.total) {
61 | this.$dispatch('items-set');
62 | }
63 | },
64 | loadMore: function () {
65 | this.$root.$broadcast('load-more-items', this.selection.items);
66 | },
67 | selectStorage: function (storage) {
68 | this.storage = storage;
69 | this.selection.items = this.$get('selection.results.' + storage);
70 | }
71 | },
72 | components: {
73 | grid: require('./grid')
74 | }
75 | };
76 |
--------------------------------------------------------------------------------
/src/js/app/model/item.js:
--------------------------------------------------------------------------------
1 | var util = require('../util');
2 | var docTypes = {
3 | text: ['txt', 'md', 'rst', 'rtf', 'odt', 'ott'],
4 | pdf: ['pdf'],
5 | word: ['doc', 'docx', 'dot', 'dotx'],
6 | excel: ['xls', 'xlsx', 'xlt', 'xltx'],
7 | powerpoint: ['ppt', 'pptx', 'pot', 'potx'],
8 | image: ['bmp', 'jpg', 'jpeg', 'png', 'gif', 'eps', 'psd', 'ai', 'tiff', 'svg'],
9 | archive: ['tar', 'tar.gz', 'tar.bz', 'tgz', 'bz2', 'cab', 'zip', 'zipx', 'rar', 'jar', '7z'],
10 | audio: ['3pg', 'aac', 'aiff', 'flac', 'm4a', 'mp3', 'ogg', 'wav', 'wma'],
11 | video: ['webm', 'flv', 'avi', 'mov', 'wmv', 'mp4', 'm4v', 'mpg', 'mpeg'],
12 | code: ['aspx', 'json', 'jsp', 'js', 'htm', 'html', 'php', 'phtml', 'inc', 'go', 'pl', 'asp', 'py', 'rdf', 'xml', 'svg', 'css', 'scss', 'bat', 'sh', 'c', 'h', 'rb', 'cmd', 'wsdl', 'vb', 'xslt', 'hs', 'coffee', 'go', 'yml', 'yaml', 'ini']
13 | };
14 | function MediaType(fileType, extension, mediaType) {
15 | if (fileType === 'file') {
16 | for (var key in docTypes) {
17 | if (docTypes.hasOwnProperty(key)) {
18 | if (docTypes[key].indexOf(extension) > -1) {
19 | this.name = key;
20 | break;
21 | }
22 | }
23 | }
24 | } else {
25 | this.name = 'folder';
26 | }
27 | if (mediaType) {
28 | this.icon = mediaType.icon;
29 | this.iconBig = mediaType.iconBig;
30 | this.label = mediaType.label;
31 | }
32 | }
33 | MediaType.prototype.toString = function() {
34 | return this.name || '';
35 | };
36 |
37 | module.exports = function (data, thumbnailConfig) {
38 | if (typeof data === 'function') {
39 | data = data();
40 | }
41 | if (!data.id) {
42 | throw 'Item requires an ID';
43 | }
44 | if (!data.storage) {
45 | throw 'Item requires the storage ID';
46 | }
47 | if (thumbnailConfig === 'data') {
48 | util.getImageDataUri(data.thumbnail, function (dataUri) {
49 | item.thumbnail = dataUri;
50 | });
51 | delete data.thumbnail;
52 | }
53 | var ext = data.type === 'file' ? (data.hasOwnProperty('extension') ? data.extension : (data.name.match(/\.([0-9a-z]+)$/i) || []).pop()) : undefined;
54 | var item = {
55 | id: data.id,
56 | storage: data.storage,
57 | query: data.query,
58 | name: data.name,
59 | type: data.type,
60 | extension: ext,
61 | thumbnail: data.thumbnail,
62 | mediaType: new MediaType(data.type, ext, data.mediaType),
63 | links: data.links,
64 | created: data.created,
65 | modified: data.modified,
66 | data: data.data
67 | };
68 | return item;
69 | };
70 |
--------------------------------------------------------------------------------
/src/sass/components/_items-grid.scss:
--------------------------------------------------------------------------------
1 | .items-grid {
2 | margin:15px -15px 15px 0;
3 | .item {
4 | display: inline-block;
5 | min-width:128px;
6 | padding-right:15px;
7 | cursor:default;
8 | .item-inner:hover, &.selected .item-inner, &.contextmenu .item-inner {
9 | // IE fails displaying the svg backgrounds with this effect
10 | // see the hack at the very bottom
11 | box-shadow: 0 2px 8px 0 rgba(0,0,0,.25);
12 | }
13 | &.selected .item-inner {
14 | background: $gray-lighter;
15 | }
16 | .glyphicon {
17 | font-size:32px;
18 | }
19 | .item-preview {
20 | position: relative;
21 | margin: 0;
22 | width: 100%;
23 | height: auto;
24 | background: #fff;
25 | &:before {
26 | display: block;
27 | content: "";
28 | width: 100%;
29 | padding-top: (3 / 4) * 100%;
30 | }
31 | .file-type {
32 | position: absolute;
33 | width: auto;
34 | height: 42px;
35 | left:0;
36 | right:0;
37 | top:50%;
38 | margin: 0;
39 | margin-top:-21px;
40 | &.file-type-folder {
41 | height:33px;
42 | margin-top:-16px;
43 | }
44 | }
45 | .item-thumbnail {
46 | position: absolute;
47 | left:0;
48 | right:0;
49 | top:0;
50 | bottom:0;
51 | background-size:cover;
52 | background-position: top center;
53 | &.image, &.video {
54 | background-position: center center;
55 | }
56 | }
57 | .item-icon {
58 | position: absolute;
59 | left:0;
60 | right:0;
61 | top:0;
62 | bottom:0;
63 | background-color: #fff;
64 | background-repeat: no-repeat;
65 | background-size: 48px auto;
66 | background-position: center;
67 | }
68 | }
69 | .item-title {
70 | width: 100%;
71 | white-space: nowrap;
72 | overflow: hidden;
73 | text-overflow: ellipsis; /** IE6+, Firefox 7+, Opera 11+, Chrome, Safari **/
74 | -o-text-overflow: ellipsis; /** Opera 9 & 10 **/
75 | padding:2px 6px;
76 | span, img {
77 | float:left;
78 | position: relative;
79 | top:4px;
80 | margin-right:6px;
81 | }
82 | }
83 | }
84 |
85 | .item:last-child {
86 | margin-right: 0;
87 | }
88 | }
89 |
90 |
91 | _:-ms-lang(x),
92 | .items-grid .item:hover .item-inner,
93 | .items-grid .item.selected .item-inner,
94 | .items-grid .item.contextmenu .item-inner {
95 | border:2px solid rgba(0,0,0,.25);
96 | margin:-2px;
97 | box-shadow: none;
98 | }
99 |
--------------------------------------------------------------------------------
/src/js/shared/util/messaging.js:
--------------------------------------------------------------------------------
1 | var uid = require('./uid');
2 |
3 | module.exports = require('./createClass')({
4 | construct: function(origin, windowObject) {
5 | var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
6 | var eventer = window[eventMethod];
7 | var messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message";
8 | eventer(messageEvent, function(e) {
9 | var origin = e.origin || e.originalEvent.origin;
10 | if (e.source === this.window && origin === this.origin || this.origin === '*') {
11 | this.handle(e.data);
12 | }
13 | }.bind(this), false);
14 |
15 | this.origin = origin;
16 | this.window = windowObject;
17 | this.servers = {};
18 | this._handlers = {};
19 | },
20 | registerServer: function (name, object) {
21 | this.servers[name] = object;
22 | },
23 | _createHandler: function() {
24 | var handler = { callbacks: [] };
25 | handler.then = function (callback) {
26 | if (handler.hasOwnProperty('_result')) {
27 | callback(handler._result);
28 | } else {
29 | handler.callbacks.push(callback);
30 | }
31 | return handler;
32 | };
33 | return handler;
34 | },
35 | call: function(method) {
36 | var arguments = Array.prototype.slice.call(arguments, 1);
37 | var id = uid(), handler = this._createHandler();
38 | this._handlers[id] = handler;
39 | this.window.postMessage({id: id, method: method, arguments: arguments}, this.origin);
40 | return handler;
41 | },
42 | handle: function(message) {
43 | if (message.method === 'resolve') {
44 | if (this._handlers[message.id]) {
45 | var handler = this._handlers[message.id];
46 | for (var i = 0, l = handler.callbacks.length; i < l; i++) {
47 | handler.callbacks[i](message.result);
48 | }
49 | handler._result = message.result;
50 | delete this._handlers[message.id];
51 | }
52 | } else {
53 | var methodPath = message.method.split('.');
54 | var method = methodPath.pop();
55 | var target = this.servers;
56 | while (target && methodPath.length) {
57 | target = target[methodPath.shift()];
58 | }
59 | if (!target || !target[method]) {
60 | throw 'Unknown method "' + message.method + '"';
61 | }
62 | var result = target[method].apply(target, message.arguments);
63 | var resolve = function(res) {
64 | // It might occure, that the id is reset, when the target frame is removed
65 | if (message.id) {
66 | this.window.postMessage({method: 'resolve', id: message.id, result: res}, this.origin);
67 | }
68 | }.bind(this);
69 | if (typeof result === 'function') {
70 | result(resolve);
71 | } else {
72 | resolve(result);
73 | }
74 | }
75 | }
76 | });
77 |
--------------------------------------------------------------------------------
/src/js/app/util.js:
--------------------------------------------------------------------------------
1 | var Vue = require('vue');
2 | Vue.filter('encodeURI', function(data) {
3 | return encodeURI(data);
4 | });
5 | Vue.filter('encodeURIComponent', function(data) {
6 | return encodeURIComponent(data);
7 | });
8 |
9 | Array.prototype.filterBy = function (key, value) {
10 | var values = value.indexOf ? value : [value];
11 | return this.filter(function(arrayValue) {
12 | if (typeof arrayValue === "object") {
13 | return values.indexOf(arrayValue[key]) > -1;
14 | }
15 | return false;
16 | });
17 | };
18 |
19 | var params;
20 |
21 | window.getParams = function () {
22 | if (params === undefined) {
23 | params = {};
24 | var query = window.location.search.substring(1);
25 | var vars = query.split('&');
26 | for (var i = 0; i < vars.length; i++) {
27 | var pair = vars[i].split('=');
28 | params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
29 | }
30 | }
31 | return params;
32 | };
33 | window.getParam = function (name) {
34 | return window.getParams()[name];
35 | };
36 |
37 | module.exports = {
38 | /**
39 | * Render seconds to HH:MM:SS format
40 | *
41 | * @param time
42 | * @returns {string}
43 | */
44 | formatTime: function (time) {
45 | // http://stackoverflow.com/a/6313008
46 | var sec_num = parseInt(time + '', 10); // don't forget the second param
47 | var hours = Math.floor(sec_num / 3600);
48 | var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
49 | var seconds = sec_num - (hours * 3600) - (minutes * 60);
50 |
51 | if (hours < 10) {hours = "0"+hours;}
52 | if (minutes < 10) {minutes = "0"+minutes;}
53 | if (seconds < 10) {seconds = "0"+seconds;}
54 | return hours+':'+minutes+':'+seconds;
55 | },
56 | /**
57 | * Load a script
58 | *
59 | * @param {string} url
60 | * @param {function} callback
61 | */
62 | loadScript: function (url, callback) {
63 | // Adding the script tag to the head as suggested before
64 | var head = document.getElementsByTagName('head')[0];
65 | var script = document.createElement('script');
66 | script.type = 'text/javascript';
67 | script.src = url;
68 |
69 | // Then bind the event to the callback function.
70 | // There are several events for cross browser compatibility.
71 | script.onreadystatechange = callback;
72 | script.onload = callback;
73 |
74 | // Fire the loading
75 | head.appendChild(script);
76 | },
77 | /**
78 | * Get image data uri
79 | *
80 | * @param {string} url
81 | * @param {function} callback
82 | */
83 | getImageDataUri: function (url, callback) {
84 | var image = new Image();
85 |
86 | image.onload = function () {
87 | var canvas = document.createElement('canvas');
88 | canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size
89 | canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size
90 |
91 | canvas.getContext('2d').drawImage(this, 0, 0);
92 |
93 | callback(canvas.toDataURL('image/png'));
94 | };
95 |
96 | image.src = url;
97 | }
98 | };
--------------------------------------------------------------------------------
/src/sass/main.scss:
--------------------------------------------------------------------------------
1 | $gray-base: #000 !default;
2 | $gray-darker: lighten($gray-base, 13.5%) !default; // #222
3 | $gray-dark: #212121 !default;
4 | $gray: #666 !default;
5 | $gray-light: #bbb !default;
6 | $gray-lighter: lighten($gray-base, 93.5%) !default; // #eee
7 |
8 | $brand-primary: #2196F3 !default;
9 | $brand-success: #4CAF50 !default;
10 | $brand-info: #9C27B0 !default;
11 | $brand-warning: #ff9800 !default;
12 | $brand-danger: #e51c23 !default;
13 |
14 | $padding-base-vertical: 6px;
15 | $padding-large-vertical: 16px;
16 | $padding-base-horizontal: $padding-base-vertical;
17 | $padding-large-horizontal: $padding-large-vertical;
18 |
19 | $line-height: 1.846;
20 |
21 | $navbar-height: 48px;
22 | $navbar-font-size: 16px;
23 |
24 | $footer-height: $navbar-height + 2;
25 |
26 | @import url("https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/paper/bootstrap.min.css");
27 | @import "mixins/file-types";
28 | @include file-types("file-type");
29 |
30 | @import "components/navbar";
31 | @import "components/tree";
32 | @import "components/sidebar";
33 | @import "components/items";
34 | @import "components/loaders";
35 | @import "components/contextmenu";
36 |
37 | body {
38 | background: transparent;
39 | }
40 | * {
41 | -moz-user-select: -moz-none;
42 | -khtml-user-select: none;
43 | -webkit-user-select: none;
44 | user-select: none;
45 | }
46 |
47 | #app {
48 | opacity: 0;
49 | transition: opacity 0.2s;
50 | &.loaded {
51 | opacity: 1;
52 | }
53 | }
54 |
55 | #main {
56 | position: absolute;
57 | padding-top: $navbar-height;
58 | padding-bottom: $footer-height;
59 | background: lighten($gray-lighter, 5%);
60 | left:0;
61 | top:0;
62 | width:100%;
63 | height:100%;
64 | display: table;
65 | > div {
66 | display: table-cell;
67 | height: 100%;
68 | vertical-align: top;
69 | > div {
70 | width:100%;
71 | height:100%;
72 | position: relative;
73 | > div {
74 | overflow-x:hidden;
75 | overflow-y: auto;
76 | position: absolute;
77 | top:0;
78 | left:0;
79 | right:0;
80 | bottom:0;
81 | }
82 | }
83 | sidebar {
84 | width: 250px;
85 | }
86 | stage > div > div {
87 | padding: 0 15px 0 12px;
88 | }
89 | &.handle {
90 | height: 100%;
91 | width: 3px;
92 | border-left:1px solid darken($gray-lighter, 5%);
93 | cursor: col-resize;
94 | }
95 | }
96 | }
97 |
98 | #footer {
99 | position: absolute;
100 | height: $footer-height;
101 | background: lighten($gray-lighter, 5%);
102 | border-top:1px solid darken($gray-lighter, 5%);
103 | bottom:0;
104 | width:100%;
105 | padding:10px 15px;
106 | span {
107 | white-space: nowrap;
108 | overflow:hidden;
109 | margin-left:15px;
110 | position: relative;
111 | top:4px;
112 | &:first-child {
113 | margin-left:0;
114 | }
115 | }
116 | button {
117 | float:right;
118 | margin-left:15px;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/js/picker/components/modal/index.js:
--------------------------------------------------------------------------------
1 | var extend = require('extend'),
2 | util = require('../../util'),
3 | insertCss = require('insert-css'),
4 | transitionEvent = (function() {
5 | var el = document.createElement('div');
6 | var transitions = {
7 | 'transition': 'transitionend',
8 | 'OTransition': 'otransitionend',
9 | 'MozTransition': 'transitionend',
10 | 'WebkitTransition': 'webkitTransitionEnd'
11 | };
12 | for (var i in transitions) {
13 | if (transitions.hasOwnProperty(i) && el.style[i] !== undefined) {
14 | return transitions[i];
15 | }
16 | }
17 | })(),
18 | hasTransitions = function(element) {
19 | var css = window.getComputedStyle(element, null);
20 | var transitionDuration = ['transitionDuration', 'oTransitionDuration', 'MozTransitionDuration', 'webkitTransitionDuration'];
21 | var hasTransition = transitionDuration.filter(function (i) {
22 | if (typeof css[i] === 'string' && css[i].match(/[1-9]/)) {
23 | return true;
24 | }
25 | });
26 | return hasTransition.length ? true : false;
27 | },
28 | Messaging = require('../../../shared/util/messaging');
29 |
30 | module.exports = require('../../../shared/util/createClass')({
31 | construct: function (options) {
32 | this.options = extend(
33 | {
34 | template: require('./index.html'),
35 | css: require('./index.css'),
36 | openClassName: 'assetpicker-modal-open',
37 | src: null
38 | },
39 | options
40 | );
41 | this.modal = null;
42 | this.frame = null;
43 |
44 | var matches = this.options.src.match(/^https?:\/\/[^\/]+/);
45 | this.messaging = new Messaging(matches ? matches[0] : document.location.origin)
46 | },
47 | render: function() {
48 | if (this.options.css) {
49 | insertCss(this.options.css);
50 | }
51 | var div = document.createElement('div');
52 | div.innerHTML = this.options.template;
53 | this.modal = div.children[0];
54 | this.modal.addEventListener('click', function(event) {
55 | if (event.target === this.modal) {
56 | this.close();
57 | }
58 | }.bind(this));
59 | this.frame = this.modal.querySelector('iframe');
60 | document.body.appendChild(this.modal);
61 | this._modalClass = this.modal.className;
62 | },
63 | open: function() {
64 | if (!this.modal) {
65 | this.render();
66 | var that = this;
67 | this.frame.src = this.options.src;
68 | window.setTimeout(function() { that.open(); }, 1);
69 | return;
70 | }
71 | this.messaging.window = this.frame.contentWindow;
72 | util.addClass(this.modal, this.options.openClassName);
73 | },
74 | maximize: function () {
75 | util.addClass(this.modal, 'assetpicker-maximized');
76 | },
77 | minimize: function () {
78 | util.removeClass(this.modal, 'assetpicker-maximized');
79 | },
80 | _closed: function() {
81 | },
82 | close: function() {
83 | if (transitionEvent && hasTransitions(this.modal)) {
84 | var closeTransitionHandler = function () {
85 | this.modal.removeEventListener(transitionEvent, closeTransitionHandler);
86 | this._closed();
87 | }.bind(this);
88 | this.modal.addEventListener(transitionEvent, closeTransitionHandler);
89 | } else {
90 | this._closed();
91 | }
92 | util.removeClass(this.modal, this.options.openClassName);
93 | }
94 | });
95 |
--------------------------------------------------------------------------------
/src/js/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
29 |
30 |
37 |
38 |
39 |
40 |
41 |
No storages configured
42 |
43 |
44 |
45 |
46 |
47 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/dist/js/adapter/entermediadb.js:
--------------------------------------------------------------------------------
1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.AssetPickerAdapterEntermediadb=e()}}(function(){return function e(t,s,i){function a(r,o){if(!s[r]){if(!t[r]){var d="function"==typeof require&&require;if(!o&&d)return d(r,!0);if(n)return n(r,!0);var l=new Error("Cannot find module '"+r+"'");throw l.code="MODULE_NOT_FOUND",l}var h=s[r]={exports:{}};t[r][0].call(h.exports,function(e){var s=t[r][1][e];return a(s?s:e)},h,h.exports,e,t,s,i)}return s[r].exports}for(var n="function"==typeof require&&require,r=0;r0;)i.pop();this.loadAssets(i)}},immediate:!0}},dateFormat:"YYYY-MM-DDTHH:mm:ss",methods:{assembleTerms:function(){var e=[],t=function(t,s,i){e.push({field:t,operator:s,value:i})};return this.category&&t("category","exact",this.category.id),this.search&&t("description","freeform",this.search),this.extensions&&this.extensions.length&&t("fileformat","matches",this.extensions.join("|")),e.length||t("id","matches","*"),e},loadAssets:function(e){var t=this.assembleTerms(),s=JSON.stringify(t),i=this.results[s];if(i){if(e&&i.items!==e&&(Array.prototype.push.apply(e,i.items),e.total=i.items.total,e.loading=i.items.loading,e.query=s,i.items=e),i.page===i.pages)return this.$promise(function(e){e(i)})}else i={page:0,pages:0,items:e||[]},i.items.total=i.items.total||i.items.length,this.results[s]=i;return i.items.loading=!0,i.items.query=s,this.http.post("module/asset/search",{page:""+(i.page+1),hitsperpage:"20",query:{terms:t}}).then(function(e){if(i.items.query===s){i.page=parseInt(e.data.response.page),i.pages=parseInt(e.data.response.pages),i.items.total=parseInt(e.data.response.totalhits),i.items.loading=!1;var t=this.config.url.replace(/\/+$/,"")+"/emshare";e.data.results.forEach(function(e){var a=this.createItem({id:e.id,query:s,type:e.isfolder?"file":"dir",name:e.assettitle||e.name||e.primaryfile,title:e.assettitle,extension:e.fileformat.id,created:this.parseDate(e.assetcreationdate||e.assetaddeddate),modified:this.parseDate(e.assetmodificationdate),thumbnail:this.url("/emshare/views/modules/asset/downloads/preview/thumb/"+encodeURI(e.sourcepath)+"/thumb.jpg",this.config.url),links:{open:t+"/views/modules/asset/editor/viewer/index.html?assetid="+e.id,download:t+"/views/activity/downloadassets.html?assetid="+e.id},data:e});i.items.push(a)}.bind(this))}return i}.bind(this))}},events:{"select-item":function(e){return"entrypoint"!==e||(this.category=null,this.search=null,this.loadAssets().then(function(e){this.items=e.items,this.$parent.$dispatch("select-item",this)}.bind(this)),void 0)},"load-more-items":function(e){this.loadAssets(e)},search:function(e,t){return this.search=e,this.loadAssets(t),!0},"category-load-items":function(e){this.http.post("lists/search/category",{hitsperpage:"100",query:{terms:[{field:"parentid",operator:"exact",value:e.item?e.item.id:"index"}]}}).then(function(t){e.items=t.data.results.map(function(e){return this.createItem({id:e.id,name:e.name,type:"category",data:e})}.bind(this))})},"category-select-item":function(e){this.category=e.item,this.search=null,this.loadAssets(e.items).then(function(t){e.selected&&this.$dispatch("select-item",e)}.bind(this))}}}},{"./template.html":2}],2:[function(e,t,s){t.exports='\n \n
'},{}]},{},[1])(1)});
2 | //# sourceMappingURL=../maps/adapter/entermediadb.js.map
3 |
--------------------------------------------------------------------------------
/src/sass/picker-ui.scss:
--------------------------------------------------------------------------------
1 | @import "mixins/file-types";
2 |
3 | $pane-background: #eee;
4 | $title-background: lighten($pane-background, 5);
5 | $color: darken($pane-background, 53);
6 |
7 | .assetpicker-ui {
8 | margin-bottom: 1em;
9 | position: relative;
10 | overflow: hidden;
11 | padding-bottom: 6px;
12 | }
13 | .assetpicker-hidden {
14 | position: absolute;
15 | top:0;
16 | left:100%;
17 | margin-left:20px;
18 | }
19 | .assetpicker-ui > * {
20 | margin-bottom:-6px;
21 | }
22 | .assetpicker-item {
23 | vertical-align: middle;
24 | box-sizing: content-box;
25 | display: inline-block;
26 | width: 100px;
27 | background: $pane-background;
28 | padding:6px;
29 | margin-right:-6px;
30 | }
31 | .assetpicker-item:last-of-type {
32 | margin-right: 6px;
33 | }
34 | .assetpicker-item:last-child {
35 | margin-right: 0;
36 | }
37 | .assetpicker-preview {
38 | position: relative;
39 | background:#fff;
40 | padding-top: 3 / 4 * 100%;
41 | div {
42 | position: absolute;
43 | left: 0;
44 | top: 0;
45 | bottom: 0;
46 | right: 0;
47 | &.assetpicker-ft {
48 | height: 42px;
49 | width: auto;
50 | bottom: auto;
51 | top: 50%;
52 | margin: 0;
53 | margin-top: -21px;
54 | &.assetpicker-ft-folder {
55 | height: 33px;
56 | margin-top: -16px;
57 | }
58 | }
59 | &.assetpicker-tn {
60 | background-size:cover;
61 | background-position: top center;
62 | &.assetpicker-image, &.assetpicker-video {
63 | background-position: center center;
64 | }
65 | }
66 | &.assetpicker-icn {
67 | background-color: #fff;
68 | background-repeat: no-repeat;
69 | background-size: 48px auto;
70 | background-position: center;
71 | }
72 | }
73 | }
74 | .assetpicker-title {
75 | font-size:12px;
76 | background: $title-background;
77 | padding:3px 6px;
78 | white-space: nowrap;
79 | overflow: hidden;
80 | text-overflow: ellipsis; /** IE6+, Firefox 7+, Opera 11+, Chrome, Safari **/
81 | -o-text-overflow: ellipsis; /** Opera 9 & 10 **/
82 | -moz-user-select: -moz-none;
83 | -khtml-user-select: none;
84 | -webkit-user-select: none;
85 | user-select: none;
86 | cursor: default;
87 | color: $color;
88 | span, img {
89 | float:left;
90 | position: relative;
91 | top:4px;
92 | margin-right:6px;
93 | }
94 | }
95 | .assetpicker-del {
96 | cursor: pointer;
97 | display:block;
98 | position: absolute;
99 | border-radius: 2px;
100 | $width:8;
101 | $padding: 3px;
102 | background: svg-url('') $pane-background no-repeat center;
106 | opacity: 0;
107 | width: $width + (2 * $padding);
108 | height: $width + (2 * $padding);
109 | right:6px;
110 | top:6px;
111 | transition: all 0.3s;
112 | }
113 | .assetpicker-item:hover .assetpicker-del {
114 | opacity: 0.8;
115 | }
116 | .assetpicker-item:hover .assetpicker-del:hover {
117 | opacity: 1;
118 | }
119 | .assetpicker-add {
120 | box-sizing: border-box;
121 | display: inline-block;
122 | vertical-align: middle;
123 | cursor: pointer;
124 | $width:13;
125 | $x: $width / 2;
126 | $padding: 12px;
127 | width: $width + (2 * $padding);
128 | height: $width + (2 * $padding);
129 | background: svg-url('') $pane-background no-repeat center;
133 | border:2px solid $pane-background;
134 | transition: all 0.3s;
135 | margin-top:4px;
136 | margin-bottom:-2px;
137 | }
138 | .assetpicker-add:hover {
139 | background-color: $title-background;
140 | }
141 | @include file-types("assetpicker-ft");
142 |
--------------------------------------------------------------------------------
/src/js/app/locales.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | header: {
3 | title: {
4 | en: 'Explorer',
5 | de: 'Explorer'
6 | },
7 | search: {
8 | en: 'Search',
9 | de: 'Suchen'
10 | },
11 | minimize: {
12 | en: 'Minimize',
13 | de: 'Verkleinern'
14 | },
15 | maximize: {
16 | en: 'Maximize',
17 | de: 'Maximieren'
18 | }
19 | },
20 | login: {
21 | username: {
22 | en: 'User name',
23 | de: 'Benutzername'
24 | },
25 | password: {
26 | en: 'Password',
27 | de: 'Passwort'
28 | },
29 | login: {
30 | en: 'Login',
31 | de: 'Anmelden'
32 | },
33 | failure: {
34 | en: 'Your username or password were wrong',
35 | de: 'Benutzername oder Passwort sind falsch'
36 | }
37 | },
38 | types: {
39 | file: {
40 | en: 'File',
41 | de: 'Datei'
42 | },
43 | dir: {
44 | en: 'Directory',
45 | de: 'Verzeichnis'
46 | },
47 | category: {
48 | en: 'Category',
49 | de: 'Kategorie'
50 | }
51 | },
52 | descriptor: {
53 | type: {
54 | en: 'Item type',
55 | de: 'Elementtyp'
56 | },
57 | path: {
58 | en: 'Path',
59 | de: 'Pfad'
60 | },
61 | id: {
62 | en: 'ID',
63 | de: 'ID'
64 | },
65 | dimensions: {
66 | en: 'Dimensions',
67 | de: 'Abmessungen'
68 | },
69 | created: {
70 | en: 'Creation date',
71 | de: 'Erstellungsdatum'
72 | },
73 | modified: {
74 | en: 'Modification date',
75 | de: 'Änderungsdatum'
76 | },
77 | length: {
78 | en: 'Length',
79 | de: 'Länge'
80 | },
81 | pages: {
82 | en: 'Pages',
83 | de: 'Seiten'
84 | }
85 | },
86 | link: {
87 | download: {
88 | en: 'Download',
89 | de: 'Herunterladen'
90 | },
91 | open: {
92 | en: 'Open',
93 | de: 'Öffnen'
94 | }
95 | },
96 | stage: {
97 | nothingFound: {
98 | en: 'No proper results found',
99 | de: 'Keine passenden Ergebnisse gefunden'
100 | },
101 | noItems: {
102 | en: 'No items',
103 | de: 'Keine Elemente'
104 | }
105 | },
106 | footer: {
107 | pick: {
108 | en: 'Select',
109 | de: 'Auswählen'
110 | },
111 | cancel: {
112 | en: 'Cancel',
113 | de: 'Abbrechen'
114 | },
115 | loading: {
116 | en: 'Loading...',
117 | de: 'Lade...'
118 | },
119 | searching: {
120 | en: 'Searching...',
121 | de: 'Suche...'
122 | },
123 | items: {
124 | en: '{{summary.numItems}} item{{summary.numItems !== 1 ? "s" : ""}}',
125 | de: '{{summary.numItems}} Element{{summary.numItems !== 1 ? "e" : ""}}'
126 | },
127 | results: {
128 | en: '{{summary.numItems}} result{{summary.numItems !== 1 ? "s" : ""}}',
129 | de: '{{summary.numItems}} Ergebnis{{summary.numItems !== 1 ? "se" : ""}}'
130 | },
131 | storages: {
132 | en: '{{numStorages}} Storages',
133 | de: '{{numStorages}} Speicher'
134 | },
135 | picked: {
136 | en: '{{picked.length}} item{{picked.length !== 1 ? "s" : ""}} picked',
137 | de: '{{picked.length}} Element{{picked.length !== 1 ? "e" : ""}} ausgewählt'
138 | },
139 | resultsOverview: {
140 | en: '{{$interpolate(t("footer.results")) + " in " + summary.numStorages + " storage" + (summary.numStorages !== 1 ? "s" : "")}}',
141 | de: '{{$interpolate(t("footer.results")) + " in " + summary.numStorages + " Speicher" + (summary.numStorages !== 1 ? "n" : "")}}'
142 | }
143 | },
144 | date: {
145 | // https://github.com/taylorhakes/fecha#formatting-tokens
146 | full: {
147 | en: 'MM/DD/YYYY HH:mm',
148 | de: 'DD.MM.YYYY HH:mm'
149 | }
150 | }
151 | };
152 |
--------------------------------------------------------------------------------
/src/js/app/components/tree/index.js:
--------------------------------------------------------------------------------
1 | var Vue = require('vue');
2 | var escapeRegExp = require('escape-string-regexp');
3 |
4 | var selected;
5 |
6 | Vue.component('tree', {
7 | template: require('./tree.html'),
8 | props: {
9 | item: Object,
10 | open: Boolean,
11 | name: String,
12 | selected: Boolean,
13 | fetch: Boolean,
14 | entryPoint: String,
15 | main: Boolean,
16 | types: {
17 | type: Array,
18 | default: function() {
19 | return ['dir']
20 | }
21 | },
22 | items: {
23 | type: Array,
24 | default: function () {
25 | return [];
26 | }
27 | }
28 | },
29 | data: function () {
30 | return {
31 | search: this.$parent.search || {
32 | sword: null,
33 | results: null
34 | }
35 | }
36 | },
37 | computed: {
38 | prefix: function () {
39 | return this.name ? this.name + '-' : '';
40 | }
41 | },
42 | created: function () {
43 | if (!this.entryPoint) {
44 | this.$dispatch(this.prefix + 'load-items', this);
45 | }
46 | },
47 | events: {
48 | 'search': function (sword, results) {
49 | this.search.sword = sword;
50 | this.search.results = results;
51 | this.doSearch();
52 | return true;
53 | },
54 | 'select-item': function (item) {
55 | if (item instanceof Vue) {
56 | if (item.entryPoint) {
57 | this.$nextTick(function () {
58 | this.$broadcast('select-item', 'entrypoint');
59 | });
60 | return false;
61 | }
62 | item = item.item;
63 | if (!item) {
64 | return true;
65 | }
66 | }
67 | if (item === 'entrypoint' && !this.item && !this.entryPoint) {
68 | this.select(false);
69 | return false;
70 | }
71 | if (item && this.item && item.id === this.item.id || this.entryPoint && !item.id) {
72 | this.select();
73 | return false;
74 | } else if (item) {
75 | for (var i = 0, l = this.items.length; i < l; i++) {
76 | if (this.items[i].id === item.id) {
77 | if (!this.open && !this.entryPoint) {
78 | this.open = true;
79 | this.$nextTick(function () {
80 | this.$broadcast('select-item', item);
81 | });
82 | return false;
83 | }
84 | break;
85 | }
86 | }
87 | }
88 | return true;
89 | },
90 | '_open': function () {
91 | this.open = true;
92 | return true;
93 | },
94 | 'deselect-items': function () {
95 | this.selected = false;
96 | return true;
97 | }
98 | },
99 | methods: {
100 | doSearch: function () {
101 | if (this.search.sword) {
102 | var regex = new RegExp(escapeRegExp(this.search.sword), 'i');
103 | for (var i = 0, l = this.items.length; i < l ; i++) {
104 | if (regex.test(this.items[i].name)) {
105 | this.search.results.push(this.items[i]);
106 | }
107 | }
108 | }
109 | },
110 | select: function (doSwitch) {
111 | if (doSwitch !== false) {
112 | if (selected && selected !== this) {
113 | selected.selected = false;
114 | }
115 | selected = this;
116 | }
117 | this.selected = true;
118 | (this.entryPoint ? this : this.$parent).$dispatch(this.prefix + 'select-item', this);
119 | this.$parent.$dispatch('_open');
120 | }
121 | },
122 | watch: {
123 | items: function (items) {
124 | if (this.selected) {
125 | this.$nextTick(function () {
126 | this.$parent.$dispatch(this.prefix + 'select-item', this);
127 | });
128 | }
129 | this.$nextTick(function () {
130 | this.doSearch();
131 | })
132 | }
133 | },
134 | components: []
135 | });
136 |
--------------------------------------------------------------------------------
/src/js/adapter/github/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | translations: {
3 | description: {
4 | en: 'Repository {{config.username}}/{{config.repository}} on GitHub',
5 | de: 'Repository {{config.username}}/{{config.repository}} auf GitHub'
6 | }
7 | },
8 | http: {
9 | base: 'https://api.github.com'
10 | },
11 | data: function() {
12 | return {
13 | // this.appConfig is not yet available here, so have to initialize it on created
14 | token: null
15 | };
16 | },
17 | created: function () {
18 | this.token = this.appConfig.github.token || localStorage.getItem('github_token')
19 | },
20 | watch: {
21 | token: function (token) {
22 | if (token) {
23 | localStorage.setItem('github_token', token);
24 | } else if (localStorage.getItem('github_token')) {
25 | localStorage.removeItem('github_token')
26 | }
27 | }
28 | },
29 | events: {
30 | 'load-items': function (tree) {
31 | if (this.token) {
32 | this.http.get(
33 | 'repos/' + this.config.username + '/' + this.config.repository + '/contents/' + (tree.item ? tree.item.id : ''),
34 | {
35 | headers: {
36 | Authorization: 'token ' + this.token
37 | }
38 | }
39 | ).then(
40 | function(response) {
41 | var items = response.data.map((function (file) {
42 | return this.createItem({
43 | id: file.path.replace(/^\/+/, ''),
44 | name: file.name,
45 | type: file.type,
46 | data: file,
47 | links: {
48 | open: file.html_url
49 | }
50 | });
51 | }).bind(this));
52 | tree.items = this.sortItems(items);
53 | },
54 | (function () {
55 | this.token = null;
56 | this.$dispatch('load-items', tree);
57 | }).bind(this)
58 | );
59 | } else {
60 | this.createToken().then((function () {
61 | this.$dispatch('load-items', tree);
62 | }).bind(this));
63 | }
64 | }
65 | },
66 | methods: {
67 | createToken: function () {
68 | return this.login(function (username, password, callback) {
69 | var baseUrl = document.location.protocol + '//' + document.location.host,
70 | fingerprint = 'netresearch-assetpicker-github-' + baseUrl,
71 | options = {
72 | headers: {
73 | Authorization: 'Basic ' + btoa(username + ':' + password)
74 | }
75 | },
76 | createAuthorization = function () {
77 | this.http.post(
78 | 'authorizations',
79 | {
80 | note: 'Repository access for ' + this.t('header.title') + ' at ' + baseUrl,
81 | scopes: ['public_repo', 'repo'],
82 | fingerprint: fingerprint
83 | },
84 | options
85 | ).then(
86 | function (response) {
87 | this.token = response.data.token;
88 | if (!this.token) {
89 | throw 'Could not find expected this.token';
90 | }
91 | callback(true);
92 | }.bind(this)
93 | )
94 | }.bind(this);
95 |
96 | this.http.get('authorizations', options).then(
97 | (function(response) {
98 | for (var i = 0, l = response.data.length; i < l; i++) {
99 | if (response.data[i].fingerprint === fingerprint) {
100 | this.http.delete('authorizations/' + response.data[i].id, options).then(createAuthorization);
101 | return;
102 | }
103 | }
104 | createAuthorization();
105 | }).bind(this),
106 | function () {
107 | callback(false);
108 | }
109 | );
110 | });
111 | }
112 | }
113 | };
114 |
--------------------------------------------------------------------------------
/src/js/app/components/items/grid.js:
--------------------------------------------------------------------------------
1 | var fecha = require('fecha');
2 | var util = require('../../util');
3 |
4 | module.exports = {
5 | template: require('./grid.html'),
6 | props: {
7 | items: Array,
8 | storage: String,
9 | limit: Number
10 | },
11 | data: function() {
12 | return {
13 | containerClass: null
14 | }
15 | },
16 | events: {
17 | 'resize': function() {
18 | this.updateLayoutClass();
19 | }
20 | },
21 | attached: function() {
22 | this.updateLayoutClass();
23 | },
24 | methods: {
25 | updateLayoutClass: function() {
26 | var itemMinWidth = this.getItemMinWidth(), availableWidth = this.$el.offsetWidth;
27 | if (itemMinWidth) {
28 | var availableColumns = Math.floor(availableWidth / itemMinWidth);
29 | var itemPercentWidth = Math.round(100 / availableColumns * 1000) / 1000;
30 | for (var i = 0, l = this.$children.length; i < l; i++) {
31 | this.$children[i].$el.style.width = itemPercentWidth + '%';
32 | }
33 | }
34 | },
35 | getItemMinWidth: function () {
36 | if (this.itemWidth === undefined && this.$children.length) {
37 | var item = this.$children[0].$el;
38 | this.itemMinWidth = null;
39 | var css = window.getComputedStyle(item).getPropertyValue('min-width');
40 | if (!css || !css.match(/px$/)) {
41 | console.warn('.' + item.className.replace(/ /, '.') + ' is supposed to have a min-width css property in px');
42 | return;
43 | }
44 | this.itemMinWidth = parseInt(css.replace(/^([0-9]+)px$/, '$1'));
45 | }
46 | return this.itemMinWidth;
47 | }
48 | },
49 | watch: {
50 | items: function() {
51 | this.$nextTick(this.updateLayoutClass);
52 | }
53 | },
54 | components: {
55 | item: {
56 | props: {
57 | item: Object
58 | },
59 | data: function () {
60 | return {
61 | picked: require('../../model/pick')
62 | }
63 | },
64 | mixins: [require('../../mixin/contextmenu')],
65 | contextmenu: function() {
66 | var items = [], icons = {
67 | open: 'new-window',
68 | download: 'floppy-save'
69 | };
70 | if (this.item.links) {
71 | for (var key in icons) {
72 | if (this.item.links.hasOwnProperty(key) && this.item.links[key]) {
73 | var html = ' ';
74 | html += this.t('link.' + key);
75 | items.push({label: html, link: this.item.links[key]});
76 | }
77 | }
78 | }
79 | return items;
80 | },
81 | computed: {
82 | selected: function() {
83 | return this.picked.contains(this.item);
84 | },
85 | title: function () {
86 | var fields = {}, item = this.item;
87 | fields.type = item.mediaType.label || (item.type === 'file' && item.extension ? item.extension.toUpperCase() + '-' : '') + this.t('types.' + item.type);
88 | if (item.id.split('/').pop() === item.name) {
89 | fields.path = ('/' + item.id).replace(/^\/+/, '/');
90 | } else {
91 | fields.id = item.id;
92 | }
93 | if (item.created) {
94 | fields.created = fecha.format(item.created, this.t('date.full'));
95 | }
96 | if (item.modified && (!item.created || item.modified > item.created)) {
97 | fields.modified = fecha.format(item.modified, this.t('date.full'));
98 | }
99 | if (item.data) {
100 | if (parseInt(item.data.width) && parseInt(item.data.height)) {
101 | fields.dimensions = item.data.width + ' x ' + item.data.height;
102 | }
103 | if (parseInt(item.data.length)) {
104 | fields.length = util.formatTime(item.data.length);
105 | }
106 | if (parseInt(item.data.pages)) {
107 | fields.pages = item.data.pages;
108 | }
109 | }
110 |
111 | var lines = [this.item.name];
112 | for (var key in fields) {
113 | if (fields.hasOwnProperty(key)) {
114 | lines.push(this.t('descriptor.' + key) + ': ' + fields[key]);
115 | }
116 | }
117 | return lines.join('\n');
118 | }
119 | },
120 | detached: function() {
121 | if (this.picked.contains(this.item)) {
122 | this.picked.remove(this.item);
123 | }
124 | },
125 | methods: {
126 | select: function() {
127 | this.picked.toggle(this.item);
128 | },
129 | open: function() {
130 | if (this.item.type === 'file' && this.picked.isAllowed(this.item)) {
131 | this.picked.add(this.item);
132 | this.$dispatch('finish-pick');
133 | } else {
134 | this.$root.$broadcast('select-item', this.item);
135 | }
136 | }
137 | }
138 | }
139 | }
140 | };
141 |
--------------------------------------------------------------------------------
/claudedocs/javascript-security-review.md:
--------------------------------------------------------------------------------
1 | # JavaScript Security Vulnerabilities - Code Review Required
2 |
3 | ## Overview
4 | This document outlines JavaScript security vulnerabilities identified by code scanning tools that require manual code review and remediation. These issues are in the frontend asset picker codebase.
5 |
6 | ## Vulnerability Summary
7 |
8 | ### 1. Prototype Pollution (6 occurrences)
9 | **Severity**: Medium to High
10 | **CWE**: CWE-1321
11 |
12 | #### Affected Files:
13 | - `/src/js/app/util.js:9` - Direct Array.prototype modification
14 | - `/src/js/shared/util/createClass.js:8,10` - result.prototype assignment
15 | - `/src/js/app/model/item.js:33` - MediaType.prototype.toString
16 | - `/src/js/app/adapter/base.js:17` - UrlClass.prototype.toString
17 | - Multiple uses of Array.prototype.slice.call() for argument conversion
18 |
19 | #### Risk:
20 | Modifying built-in prototypes can lead to prototype pollution if user-controlled data is used with these methods. Dynamic prototype manipulation could be exploited if properties contain attacker-controlled keys.
21 |
22 | #### Remediation:
23 | 1. Replace Array.prototype.filterBy with standalone utility function
24 | 2. Use Object.create() or ES6 class syntax instead of direct prototype manipulation
25 | 3. Use rest parameters instead of Array.prototype.slice.call(arguments)
26 | 4. Implement Object.freeze() on critical prototypes
27 | 5. Add input validation for all prototype operations
28 |
29 | ### 2. XSS Through DOM (2 occurrences)
30 | **Severity**: High
31 | **CWE**: CWE-79
32 |
33 | #### Affected Files:
34 | - `/src/js/picker/components/modal/index.js:52` - setting element content with template
35 | - `/src/js/app/mixin/contextmenu.js:43` - setting element content with item.label
36 |
37 | #### Risk:
38 | If template or label data contains user-controlled input, XSS vulnerability exists.
39 |
40 | #### Remediation:
41 | 1. Use textContent instead of setting HTML for text-only content
42 | 2. Use DOM manipulation methods (createElement, appendChild, textContent)
43 | 3. Implement Content Security Policy headers
44 | 4. Sanitize all user input before rendering
45 | 5. Use template literals with proper escaping
46 |
47 | ### 3. Remote Property Injection (2 occurrences)
48 | **Severity**: Medium to High
49 | **CWE**: CWE-915
50 |
51 | #### Affected Files:
52 | - `/src/js/app/util.js:24,62` - window.location.search parsing and loadScript
53 | - `/src/js/shared/util/messaging.js:39,66` - postMessage usage
54 |
55 | #### Details - URL Parameter Parsing:
56 | File: `/src/js/app/util.js`
57 | - Parses URL query parameters into object without validation
58 | - Could allow __proto__ injection via URL parameters
59 |
60 | #### Details - loadScript Function:
61 | File: `/src/js/app/util.js`
62 | - Accepts arbitrary URLs without validation
63 | - Could load malicious scripts from untrusted sources
64 |
65 | #### Details - postMessage Handler:
66 | File: `/src/js/shared/util/messaging.js`
67 | - Origin validation allows wildcard ('*'), bypassing same-origin policy
68 | - Dynamic method invocation from message.method could invoke unintended methods
69 | - Arbitrary method execution if message.arguments is attacker-controlled
70 |
71 | #### Remediation:
72 | 1. Implement allowlist validation for URL parameters
73 | 2. Validate and sanitize script URLs before loading
74 | 3. Remove wildcard origin support or implement strict origin validation
75 | 4. Implement method allowlist instead of dynamic method resolution
76 | 5. Add argument validation and type checking
77 | 6. Use structured cloning with validation for postMessage data
78 |
79 | ### 4. Functionality from Untrusted Source (1 occurrence)
80 | **Severity**: High
81 | **CWE**: CWE-830
82 |
83 | #### Affected Files:
84 | - `/src/js/app/util.js:62` - loadScript function
85 |
86 | #### Risk:
87 | The loadScript utility allows loading arbitrary JavaScript from any URL without validation, enabling potential remote code execution.
88 |
89 | #### Remediation:
90 | 1. Implement Subresource Integrity (SRI) for all external scripts
91 | 2. Use CSP script-src directive to allowlist trusted domains
92 | 3. Remove dynamic script loading if possible
93 | 4. Maintain strict allowlist of permitted script sources
94 | 5. Log all script loading attempts for security monitoring
95 |
96 | ## Priority Ranking
97 |
98 | ### Critical Priority:
99 | 1. XSS vulnerabilities in modal and contextmenu
100 | 2. postMessage origin validation wildcard
101 | 3. Dynamic script loading without validation
102 |
103 | ### High Priority:
104 | 1. URL parameter parsing (prototype pollution risk)
105 | 2. Dynamic method invocation via postMessage
106 |
107 | ### Medium Priority:
108 | 1. Array.prototype modification
109 | 2. Prototype manipulation in createClass utility
110 |
111 | ## Testing Recommendations
112 |
113 | ### Prototype Pollution Testing:
114 | Test URL parameter injection by visiting with __proto__ in query string
115 | Test postMessage injection with prototype chain manipulation
116 |
117 | ### XSS Testing:
118 | Test context menu with malicious label content
119 | Test modal template with script injection attempts
120 |
121 | ### Remote Property Injection Testing:
122 | Test loadScript with external domain URLs
123 | Test postMessage method invocation with unexpected method names
124 |
125 | ## Compliance Notes
126 |
127 | ### OWASP Top 10 2021:
128 | - A03:2021 – Injection (XSS and prototype pollution)
129 | - A05:2021 – Security Misconfiguration (wildcard origin)
130 | - A08:2021 – Software and Data Integrity Failures (dynamic script loading)
131 |
132 | ### CWE Coverage:
133 | - CWE-79: Cross-site Scripting
134 | - CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes
135 | - CWE-1321: Improperly Controlled Modification of Object Prototype Attributes
136 | - CWE-830: Inclusion of Web Functionality from an Untrusted Source
137 |
138 | ## Implementation Notes
139 |
140 | This codebase uses older JavaScript patterns (ES5 era) and would benefit from:
141 | 1. Migration to ES6+ class syntax
142 | 2. Modern bundlers with security scanning
143 | 3. ESLint security plugins
144 | 4. Regular dependency audits
145 | 5. Content Security Policy headers
146 | 6. Subresource Integrity for external resources
147 |
148 | ## Next Steps
149 |
150 | 1. Fix XSS vulnerabilities in modal and contextmenu components
151 | 2. Fix postMessage origin validation (remove wildcard)
152 | 3. Implement input validation for loadScript and URL parameters
153 | 4. Add automated security testing to CI/CD
154 | 5. Consider refactoring to modern JavaScript
155 | 6. Implement CSP headers and SRI
156 | 7. Regular security audits and penetration testing
157 |
--------------------------------------------------------------------------------
/src/js/picker/components/ui/index.js:
--------------------------------------------------------------------------------
1 | var cssLoaded = false;
2 | var extend = require('extend');
3 | var util = require('../../util');
4 |
5 | module.exports = require('../../../shared/util/createClass')({
6 | construct: function (element, picker) {
7 | if (!cssLoaded) {
8 | util.loadCss(picker.getDistUrl() + '/css/picker-ui.css');
9 | }
10 |
11 | this.config = extend(
12 | {
13 | unique: true,
14 | readonly: false
15 | },
16 | picker.options.ui,
17 | {
18 | readonly: element.hasAttribute('data-ro') ? ['false', '0'].indexOf(element.getAttribute('data-ro')) === -1 : undefined,
19 | unique: element.hasAttribute('data-unique') ? ['false', '0'].indexOf(element.getAttribute('data-unique')) === -1 : undefined
20 | }
21 | );
22 |
23 | this.add = false;
24 | this.propagate = false;
25 | this.picker = picker;
26 | this.element = element;
27 | var value = element.hasAttribute('value') ? element.getAttribute('value') : undefined;
28 | if (value) {
29 | try {
30 | this.picked = JSON.parse(value);
31 | } catch (e) {
32 | this.picked = [];
33 | console.error('Error while parsing value of %s', element);
34 | }
35 | } else {
36 | this.picked =[];
37 | }
38 | if (this.picked.constructor !== Array) {
39 | this.picked = [this.picked];
40 | }
41 | this.render();
42 |
43 | var that = this;
44 | picker.on('pick', function (picked) {
45 | if (!that.propagate && this.element === element) {
46 | var pickedArr = picked.constructor !== Array ? [picked] : picked;
47 | if (that.add) {
48 | for (var i = 0; i < pickedArr.length; i++) {
49 | var contains = false;
50 | if (that.config.unique) {
51 | for (var j = 0; j < that.picked.length; j++) {
52 | if (that.picked[j].storage === pickedArr[i].storage && that.picked[j].id === pickedArr[i].id) {
53 | contains = true;
54 | break;
55 | }
56 | }
57 | }
58 | if (!contains) {
59 | that.picked.push(pickedArr[i]);
60 | }
61 | }
62 | pickedArr = that.picked;
63 | }
64 | that.pick(pickedArr);
65 | return false;
66 | }
67 | that.propagate = false;
68 | that.add = false;
69 | });
70 | },
71 | pick: function (picked) {
72 | this.picked = picked;
73 | this.propagate = true;
74 | this.picker.element = this.element;
75 | this.picker.pick(this.picker._getPickConfig(this.element).limit === 1 ? (picked.length ? picked[0] : undefined) : picked);
76 | this.render();
77 | },
78 | createElement: function (name, className) {
79 | var element = document.createElement(name);
80 | element.className = 'assetpicker-' + className.split(' ').join(' assetpicker-');
81 | return element;
82 | },
83 | render: function () {
84 | if (!this.container) {
85 | this.container = this.element.parentNode.insertBefore(this.createElement('div', 'ui'), this.element);
86 | this.element.parentNode.removeChild(this.element);
87 | }
88 | this.container.innerHTML = '';
89 | this.renderItems(this.picked);
90 | util[(this.picked.length || this.config.readonly ? 'add' : 'remove') + 'Class'](this.element, 'assetpicker-hidden');
91 | this.container.appendChild(this.element);
92 |
93 | if (this.picked.length && !this.config.readonly) {
94 | var limit = this.picker._getPickConfig(this.element).limit;
95 | if (limit === 0 || limit !== 1 && this.picked.length < limit) {
96 | this.container.appendChild(this.createElement('span', 'add')).addEventListener('click', function (e) {
97 | this.add = true;
98 | this.element.setAttribute('data-limit', limit - this.picked.length);
99 | this.picker.open(this.element);
100 | this.element.setAttribute('data-limit', limit);
101 | }.bind(this));
102 | }
103 | }
104 | },
105 | renderItems: function (picked) {
106 | for (var i = 0, l = picked.length; i < l; i++) {
107 | var item = this.container.appendChild(this.createElement('div', 'item'));
108 | var preview = item.appendChild(this.createElement('div', 'preview'));
109 | var mediaType = extend({name: 'file'}, picked[i].mediaType);
110 | var fileTypeClass = 'ft ft-' + mediaType.name;
111 | preview.appendChild(this.createElement('div', fileTypeClass));
112 | if (mediaType.iconBig) {
113 | preview.appendChild(this.createElement('div', 'icn')).style.backgroundImage = 'url(' + mediaType.iconBig + ')';
114 | }
115 | if (picked[i].thumbnail) {
116 | preview.appendChild(this.createElement('div', 'tn ' + mediaType.name)).style.backgroundImage = 'url(' + picked[i].thumbnail + ')';
117 | }
118 | if (!this.config.readonly) {
119 | preview.appendChild(this.createDeleteButton(picked[i]));
120 | }
121 | var title = item.appendChild(this.createElement('div', 'title'));
122 | if (mediaType.icon) {
123 | title.appendChild(this.createElement('img', 'icn')).src = mediaType.icon;
124 | } else {
125 | title.appendChild(this.createElement('span', fileTypeClass));
126 | }
127 | title.appendChild(document.createTextNode(picked[i].name));
128 | }
129 | },
130 | createDeleteButton: function (item) {
131 | var button = this.createElement('span', 'del');
132 | button.addEventListener('click', function () {
133 | var picked = [];
134 | for (var i = 0, l = this.picked.length; i < l; i++) {
135 | if (this.picked[i] !== item) {
136 | picked.push(this.picked[i]);
137 | }
138 | }
139 | this.pick(picked);
140 | }.bind(this));
141 | return button;
142 | }
143 | });
--------------------------------------------------------------------------------
/src/js/picker/index.js:
--------------------------------------------------------------------------------
1 | var Modal = require('./components/modal');
2 | var UI = require('./components/ui');
3 | var uid = require('../shared/util/uid');
4 | var extend = require('extend');
5 |
6 | var distUrl = (function() {
7 | var scripts = document.getElementsByTagName('script');
8 | return scripts[scripts.length - 1].src.split('/').slice(0, -2).join('/');
9 | })();
10 |
11 | module.exports = require('../shared/util/createClass')({
12 | construct: function(config, options) {
13 | this.setConfig(config);
14 | options = extend(
15 | true,
16 | {
17 | distUrl: distUrl,
18 | selector: '[rel="assetpicker"]',
19 | modal: {
20 | src: null
21 | },
22 | ui: {
23 | enabled: true
24 | }
25 | },
26 | options || {}
27 | );
28 | if (!options.modal.src) {
29 | options.modal.src = options.distUrl + '/index.html';
30 | }
31 | if (options.modal.src.match(/^https?:\/\/localhost/) || document.location.hostname === 'localhost') {
32 | options.modal.src += '?' + uid();
33 | }
34 |
35 | this.pickConfig = {};
36 | this.options = options;
37 | this.modal = null;
38 | this.element = null;
39 | this.uis = [];
40 |
41 | this._memoryEvents = {
42 | 'ready': null
43 | };
44 | this._callbacks = {};
45 |
46 | this.on('ready', function () {
47 | this.modal.modal.className += ' assetpicker-ready'
48 | });
49 | this.on('resize', function (maximize) {
50 | this.modal[maximize ? 'maximize' : 'minimize']();
51 | });
52 |
53 | document.addEventListener('DOMContentLoaded', function () {
54 | var inputs = document.querySelectorAll(this.options.selector);
55 | for (var i = 0, l = inputs.length; i < l; i++) {
56 | this.register(inputs[i]);
57 | }
58 | }.bind(this));
59 | },
60 | getOrigin: function () {
61 | return document.location.origin;
62 | },
63 | getDistUrl: function () {
64 | return this.options.distUrl;
65 | },
66 | on: function (event, callback) {
67 | if (!this._callbacks.hasOwnProperty(event)) {
68 | this._callbacks[event] = [];
69 | }
70 | this._callbacks[event].push(callback);
71 | if (this._memoryEvents[event]) {
72 | callback.apply(this, this._memoryEvents[event]);
73 | }
74 | return this;
75 | },
76 | _trigger: function (event) {
77 | var args = Array.prototype.slice.call(arguments, 1);
78 | if (this._callbacks[event]) {
79 | this._callbacks[event].forEach(function (callback) {
80 | return callback.apply(this, args);
81 | }.bind(this));
82 | }
83 | if (this._memoryEvents.hasOwnProperty(event)) {
84 | this._memoryEvents[event] = args;
85 | }
86 | },
87 | register: function (element) {
88 | if (element.hasAttribute('data-assetpicker')) {
89 | return;
90 | }
91 | element.setAttribute('data-assetpicker', 1);
92 | if (element.hasAttribute('data-ui') || this.options.ui && this.options.ui.enabled) {
93 | this.uis.push(new UI(element, this));
94 | }
95 | element.addEventListener('click', function (event) {
96 | event.preventDefault();
97 | this.open(element);
98 | }.bind(this));
99 | },
100 | _getPickConfig: function (element) {
101 | var getSplitAttr = function (attr) {
102 | var value = element.getAttribute(attr);
103 | return value.length ? value.split(',') : []
104 | };
105 | return extend({}, this.config.pick, {
106 | limit: element.hasAttribute('data-limit') ? parseInt(element.getAttribute('data-limit')) : undefined,
107 | types: element.hasAttribute('data-types') ? getSplitAttr('data-types') : undefined,
108 | extensions: element.hasAttribute('data-ext') ? getSplitAttr('data-ext') : undefined
109 | });
110 | },
111 | getUi: function (element) {
112 | for (var i = 0, l = this.uis.length; i < l; i++) {
113 | if (this.uis[i].element === element) {
114 | return this.uis[i];
115 | }
116 | }
117 | },
118 | open: function (options) {
119 | if (// http://stackoverflow.com/a/384380, options is a HTMLElement
120 | typeof HTMLElement === "object" && options instanceof HTMLElement //DOM2
121 | || options && typeof options === "object" && options !== null && options.nodeType === 1 && typeof options.nodeName==="string") {
122 |
123 | this.element = options;
124 | this.pickConfig = this._getPickConfig(options);
125 | } else {
126 | this.element = undefined;
127 | this.pickConfig = extend({}, this.config.pick, options);
128 | }
129 | if (!this.modal) {
130 | this.modal = new Modal(this.options.modal);
131 | this.modal.messaging.registerServer('picker', this);
132 | this.on(
133 | 'ready',
134 | function () {
135 | this.modal.messaging.call('app.setConfig', {pick: this.pickConfig})
136 | }
137 | );
138 | } else {
139 | try {
140 | // When this fails, it likely means the app is not ready yet and above ready listener will handle this
141 | this.modal.messaging.call('app.setConfig', {pick: this.pickConfig});
142 | } catch (e) {}
143 | }
144 | this.modal.open();
145 | },
146 | getConfig: function() {
147 | return this.config;
148 | },
149 | setConfig: function (config) {
150 | this.config = extend(
151 | true,
152 | {
153 | pick: {
154 | limit: 1,
155 | types: ['file'],
156 | extensions: []
157 | }
158 | },
159 | config
160 | );
161 | if (this.modal) {
162 | picker.modal.messaging.call('app.setConfig', this.config);
163 | }
164 | },
165 | pick: function(picked) {
166 | if (this.element) {
167 | var tagName = this.element.tagName.toLowerCase();
168 | if (tagName === 'input' && this.element.getAttribute('type') === 'button' || tagName === 'button') {
169 | this.element.setAttribute('value', picked ? JSON.stringify(picked) : '');
170 | }
171 | }
172 | this._trigger('pick', picked);
173 | if (this.modal) {
174 | this.modal.close();
175 | }
176 | }
177 | });
178 |
--------------------------------------------------------------------------------
/dist/js/maps/adapter/dummy.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["adapter/node_modules/browserify/node_modules/browser-pack/_prelude.js","adapter/src/js/adapter/dummy/index.js","adapter/dummy.js"],"names":["f","exports","module","define","amd","g","window","global","self","this","AssetPickerAdapterDummy","e","t","n","r","s","o","u","a","require","i","Error","code","l","call","length","1","translations","description","en","de","data","lastId","events","load-items","tree","mockLoad","items","createItems","load-more-items","forEach","item","push","methods","callback","$root","loading","setTimeout","bind","extension","thumbnail","createItem","id","type","name","extensions","total"],"mappings":"CAAA,SAAAA,GAAA,GAAA,gBAAAC,UAAA,mBAAAC,QAAAA,OAAAD,QAAAD,QAAA,IAAA,kBAAAG,SAAAA,OAAAC,IAAAD,UAAAH,OAAA,CAAA,GAAAK,EAAAA,GAAA,mBAAAC,QAAAA,OAAA,mBAAAC,QAAAA,OAAA,mBAAAC,MAAAA,KAAAC,KAAAJ,EAAAK,wBAAAV,MAAA,WAAA,MAAA,SAAAW,GAAAC,EAAAC,EAAAC,GAAA,QAAAC,GAAAC,EAAAC,GAAA,IAAAJ,EAAAG,GAAA,CAAA,IAAAJ,EAAAI,GAAA,CAAA,GAAAE,GAAA,kBAAAC,UAAAA,OAAA,KAAAF,GAAAC,EAAA,MAAAA,GAAAF,GAAA,EAAA,IAAAI,EAAA,MAAAA,GAAAJ,GAAA,EAAA,IAAAhB,GAAA,GAAAqB,OAAA,uBAAAL,EAAA,IAAA,MAAAhB,GAAAsB,KAAA,mBAAAtB,EAAA,GAAAuB,GAAAV,EAAAG,IAAAf,WAAAW,GAAAI,GAAA,GAAAQ,KAAAD,EAAAtB,QAAA,SAAAU,GAAA,GAAAE,GAAAD,EAAAI,GAAA,GAAAL,EAAA,OAAAI,GAAAF,EAAAA,EAAAF,IAAAY,EAAAA,EAAAtB,QAAAU,EAAAC,EAAAC,EAAAC,GAAA,MAAAD,GAAAG,GAAAf,QAAA,IAAA,GAAAmB,GAAA,kBAAAD,UAAAA,QAAAH,EAAA,EAAAA,EAAAF,EAAAW,OAAAT,IAAAD,EAAAD,EAAAE,GAAA,OAAAD,KAAAW,GAAA,SAAAP,EAAAjB,EAAAD,GCAAC,EAAAD,SACA0B,cACAC,aACAC,GAAA,gBACAC,GAAA,kBAGAC,KAAA,WACA,OACAC,OAAA,IAGAC,QACAC,aAAA,SAAAC,GACA1B,KAAAuB,QAAA,MAGAvB,KAAA2B,SAAA,WACAD,EAAAE,MAAA5B,KAAA6B,iBAGAC,kBAAA,SAAAF,GACA5B,KAAAuB,QAAA,MAGAvB,KAAA2B,SAAA,WACA3B,KAAA6B,cAAAE,QAAA,SAAAC,GACAJ,EAAAK,KAAAD,SAKAE,SACAP,SAAA,SAAAQ,GACAnC,KAAAoC,MAAAC,UACAxC,OAAAyC,WAAA,WACAH,EAAApB,KAAAf,MACAA,KAAAoC,MAAAC,WACAE,KAAAvC,MAAA,MAEAgC,KAAA,SAAAQ,EAAAC,GACA,MAAAzC,MAAA0C,YACAC,GAAA,GAAA3C,KAAAuB,SACAqB,KAAAJ,EAAA,OAAA,MACAA,UAAAA,EACAK,KAAA,WAAAL,GAAA,eAAAC,EAAA,cAAA,IAAA,IAAAzC,KAAAuB,OACAkB,UAAAA,KAGAZ,YAAA,WACA,GAAAD,MACAkB,GAAA,MAAA,MAAA,MAAA,MAAA,MAAA,OAAA,MAAA,MAAA,MAAA,OAAA,MACAlB,GAAAK,KAAAjC,KAAAgC,OACA,KAAA,GAAArB,GAAA,EAAAG,EAAAgC,EAAA9B,OAAAL,EAAAG,EAAAH,IACAiB,EAAAK,KAAAjC,KAAAgC,KAAAc,EAAAnC,IAKA,OAHAiB,GAAAK,KAAAjC,KAAAgC,KAAA,OAAA,yCACAJ,EAAAK,KAAAjC,KAAAgC,KAAA,OAAA,yCACAJ,EAAAmB,MAAA,GAAAnB,EAAAZ,OACAY,eCMW,IAAI","file":"../../adapter/dummy.js","sourcesContent":["(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require==\"function\"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error(\"Cannot find module '\"+o+\"'\");throw f.code=\"MODULE_NOT_FOUND\",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require==\"function\"&&require;for(var o=0;o= 1400) {\n return;\n }\n this.mockLoad(function() {\n tree.items = this.createItems();\n });\n },\n 'load-more-items': function (items) {\n if (this.lastId >= 1400) {\n return;\n }\n this.mockLoad(function() {\n this.createItems().forEach(function(item) {\n items.push(item);\n });\n });\n }\n },\n methods: {\n mockLoad: function(callback) {\n this.$root.loading++;\n window.setTimeout(function() {\n callback.call(this);\n this.$root.loading--;\n }.bind(this), 500);\n },\n item: function(extension, thumbnail) {\n return this.createItem({\n id: '' + (this.lastId++),\n type: extension ? 'file' : 'dir',\n extension: extension,\n name: 'Random ' + (extension || ' directory') + (thumbnail ? ' with thumb' : '') + ' ' + this.lastId,\n thumbnail: thumbnail\n });\n },\n createItems: function () {\n var items = [],\n extensions = ['txt', 'pdf', 'xls', 'doc', 'pot', 'jpeg', 'zip', 'mp3', 'avi', 'html', 'any'];\n items.push(this.item());\n for (var i = 0, l = extensions.length; i < l; i++) {\n items.push(this.item(extensions[i]));\n }\n items.push(this.item('jpeg', 'http://lorempixel.com/nature/160/200'));\n items.push(this.item('jpeg', 'http://lorempixel.com/nature/200/160'));\n items.total = 10 * items.length;\n return items;\n }\n }\n};\n","(function(f){if(typeof exports===\"object\"&&typeof module!==\"undefined\"){module.exports=f()}else if(typeof define===\"function\"&&define.amd){define([],f)}else{var g;if(typeof window!==\"undefined\"){g=window}else if(typeof global!==\"undefined\"){g=global}else if(typeof self!==\"undefined\"){g=self}else{g=this}g.AssetPickerAdapterDummy = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require==\"function\"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error(\"Cannot find module '\"+o+\"'\");throw f.code=\"MODULE_NOT_FOUND\",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require==\"function\"&&require;for(var o=0;o= 1400) {\n return;\n }\n this.mockLoad(function() {\n tree.items = this.createItems();\n });\n },\n 'load-more-items': function (items) {\n if (this.lastId >= 1400) {\n return;\n }\n this.mockLoad(function() {\n this.createItems().forEach(function(item) {\n items.push(item);\n });\n });\n }\n },\n methods: {\n mockLoad: function(callback) {\n this.$root.loading++;\n window.setTimeout(function() {\n callback.call(this);\n this.$root.loading--;\n }.bind(this), 500);\n },\n item: function(extension, thumbnail) {\n return this.createItem({\n id: '' + (this.lastId++),\n type: extension ? 'file' : 'dir',\n extension: extension,\n name: 'Random ' + (extension || ' directory') + (thumbnail ? ' with thumb' : '') + ' ' + this.lastId,\n thumbnail: thumbnail\n });\n },\n createItems: function () {\n var items = [],\n extensions = ['txt', 'pdf', 'xls', 'doc', 'pot', 'jpeg', 'zip', 'mp3', 'avi', 'html', 'any'];\n items.push(this.item());\n for (var i = 0, l = extensions.length; i < l; i++) {\n items.push(this.item(extensions[i]));\n }\n items.push(this.item('jpeg', 'http://lorempixel.com/nature/160/200'));\n items.push(this.item('jpeg', 'http://lorempixel.com/nature/200/160'));\n items.total = 10 * items.length;\n return items;\n }\n }\n};\n\n},{}]},{},[1])(1)\n});\n\n"]}
--------------------------------------------------------------------------------
/claudedocs/SECURITY_FIXES_SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Security Fixes Summary - assetpicker
2 |
3 | ## Completed Tasks
4 |
5 | ### 1. Composer Dependency Vulnerabilities - FIXED
6 | **Status**: All 7 Composer vulnerabilities resolved
7 |
8 | #### Upgraded Packages:
9 | - **guzzlehttp/guzzle**: 5.3.1 → 6.5.8
10 | - Fixed: Cookie leakage vulnerability
11 | - Fixed: Auth header issues
12 | - Fixed: Multiple HTTP client security vulnerabilities
13 |
14 | - **jenssegers/proxy**: 2.2.1 → 3.0.2
15 | - Updated to latest version
16 | - Removed dependency on vulnerable symfony/http-foundation 2.x
17 |
18 | #### Removed Packages (obsolete/replaced):
19 | - guzzlehttp/ringphp 1.1.0 (abandoned)
20 | - guzzlehttp/streams 3.0.0 (abandoned)
21 | - ircmaxell/password-compat (no longer needed)
22 | - react/promise (replaced with guzzlehttp/promises)
23 | - symfony/http-foundation 2.8.52 (vulnerable version removed)
24 | - Old symfony polyfills (updated to 1.33.0)
25 |
26 | #### New Dependencies:
27 | - guzzlehttp/promises 1.5.3
28 | - guzzlehttp/psr7 1.9.1
29 | - psr/http-factory 1.1.0
30 | - psr/http-message 1.1
31 | - ralouphie/getallheaders 3.0.3
32 | - relay/relay 1.1.0
33 | - symfony/polyfill-intl-idn 1.33.0
34 | - symfony/polyfill-intl-normalizer 1.33.0
35 | - zendframework/zend-diactoros 2.2.1
36 |
37 | #### Verification:
38 | ```bash
39 | composer audit
40 | # Output: No security vulnerability advisories found.
41 | ```
42 |
43 | #### Configuration Changes:
44 | Added platform configuration to composer.json to allow installation with modern PHP versions while maintaining compatibility:
45 | ```json
46 | "config": {
47 | "platform": {
48 | "php": "7.4.33"
49 | }
50 | }
51 | ```
52 |
53 | ### 2. GitHub Actions Security - NOT APPLICABLE
54 | **Status**: No workflow files found
55 |
56 | The repository does not contain any GitHub Actions workflow files (.github/workflows/*.yml), so no action pinning or permissions configuration was required.
57 |
58 | ### 3. JavaScript Code Scanning Vulnerabilities - DOCUMENTED
59 | **Status**: Requires manual code review and remediation
60 |
61 | Created comprehensive security documentation: `/claudedocs/javascript-security-review.md`
62 |
63 | #### Vulnerability Breakdown:
64 | - **6 Prototype Pollution** vulnerabilities (CWE-1321)
65 | - **2 XSS Through DOM** vulnerabilities (CWE-79)
66 | - **2 Remote Property Injection** vulnerabilities (CWE-915)
67 | - **1 Functionality from Untrusted Source** vulnerability (CWE-830)
68 |
69 | #### Critical Issues Requiring Immediate Attention:
70 | 1. XSS in modal component (`/src/js/picker/components/modal/index.js:52`)
71 | 2. XSS in context menu (`/src/js/app/mixin/contextmenu.js:43`)
72 | 3. postMessage wildcard origin validation (`/src/js/shared/util/messaging.js`)
73 | 4. Unvalidated dynamic script loading (`/src/js/app/util.js:62`)
74 |
75 | #### Why Manual Review Required:
76 | These vulnerabilities are in the application's core JavaScript logic and require:
77 | - Code refactoring to use safer APIs
78 | - Input validation implementation
79 | - Security policy configuration (CSP headers)
80 | - Potential architectural changes
81 |
82 | Automated fixes could break functionality, so manual developer review is essential.
83 |
84 | ## Git Changes
85 |
86 | ### Branch Created:
87 | `security/fix-composer-vulnerabilities`
88 |
89 | ### Commits:
90 | 1. **48b5382** - Fix Composer dependency vulnerabilities
91 | - Updated composer.json and composer.lock
92 | - Upgraded all vulnerable dependencies
93 |
94 | 2. **25fc009** - Add comprehensive JavaScript security vulnerability documentation
95 | - Created detailed analysis of JS vulnerabilities
96 | - Included remediation recommendations
97 | - Added testing procedures and compliance mapping
98 |
99 | ## What Was Fixed
100 |
101 | ### Completely Resolved:
102 | - All 7 Composer dependency vulnerabilities
103 | - symfony/http-foundation PATH_INFO parsing vulnerability (indirect)
104 | - guzzlehttp/guzzle cookie leakage vulnerability
105 | - guzzlehttp/guzzle authentication header issues
106 |
107 | ### Dependencies Now Secure:
108 | - HTTP client library upgraded to secure version (6.5.8)
109 | - All abandoned packages removed or replaced
110 | - Modern PSR-compliant dependencies installed
111 |
112 | ## What Still Needs Attention
113 |
114 | ### JavaScript Vulnerabilities (11 alerts):
115 | These require manual code fixes by developers:
116 |
117 | #### High Priority:
118 | 1. **XSS Vulnerabilities** (2 files)
119 | - Replace unsafe DOM manipulation with safe methods
120 | - Implement input sanitization
121 | - Add Content Security Policy headers
122 |
123 | 2. **postMessage Security** (1 file)
124 | - Remove wildcard origin support
125 | - Implement strict origin validation
126 | - Add method allowlist for dynamic invocation
127 |
128 | 3. **Dynamic Script Loading** (1 file)
129 | - Implement URL validation and allowlisting
130 | - Add Subresource Integrity (SRI)
131 | - Configure CSP script-src directive
132 |
133 | #### Medium Priority:
134 | 4. **Prototype Pollution** (6 occurrences)
135 | - Refactor Array.prototype modifications to utility functions
136 | - Use ES6 classes instead of prototype manipulation
137 | - Add input validation for object property access
138 |
139 | ### Abandoned Dependencies:
140 | - **zendframework/zend-diactoros** is abandoned
141 | - Suggested replacement: laminas/laminas-diactoros
142 | - Not critical for security, but should be addressed in future updates
143 |
144 | ## Recommendations
145 |
146 | ### Immediate Actions:
147 | 1. Review and fix critical XSS vulnerabilities in JavaScript
148 | 2. Fix postMessage origin validation
149 | 3. Implement CSP headers in web server configuration
150 | 4. Add input validation to all user-facing JavaScript functions
151 |
152 | ### Short-term Actions:
153 | 1. Migrate to ES6+ JavaScript syntax
154 | 2. Implement automated security scanning in CI/CD
155 | 3. Add ESLint with security plugins
156 | 4. Replace abandoned zendframework dependency
157 |
158 | ### Long-term Actions:
159 | 1. Consider modernizing frontend stack (webpack, vite, etc.)
160 | 2. Implement regular security audits
161 | 3. Add penetration testing for web application
162 | 4. Maintain dependency update schedule
163 |
164 | ## Testing Verification
165 |
166 | ### Composer Security:
167 | ```bash
168 | cd /home/cybot/projects/netresearch-security-fixes/assetpicker
169 | composer audit
170 | # Result: No security vulnerability advisories found
171 | ```
172 |
173 | ### Package Versions:
174 | ```bash
175 | composer show | grep -E "guzzlehttp|symfony|jenssegers"
176 | # guzzlehttp/guzzle: 6.5.8
177 | # jenssegers/proxy: 3.0.2
178 | # symfony polyfills: 1.33.0
179 | ```
180 |
181 | ## Files Modified
182 |
183 | ### Updated:
184 | - `/composer.json` - Added platform config
185 | - `/composer.lock` - Updated dependency tree
186 |
187 | ### Created:
188 | - `/claudedocs/javascript-security-review.md` - JS vulnerability documentation
189 | - `/claudedocs/SECURITY_FIXES_SUMMARY.md` - This summary document
190 |
191 | ## Next Steps for Developer
192 |
193 | 1. Review the feature branch: `security/fix-composer-vulnerabilities`
194 | 2. Test the application with updated dependencies
195 | 3. Read JavaScript security documentation in `/claudedocs/`
196 | 4. Prioritize fixing critical XSS vulnerabilities
197 | 5. Implement CSP headers
198 | 6. Create follow-up issues for each JavaScript vulnerability
199 | 7. Merge the branch after testing
200 | 8. Plan JavaScript code refactoring sprint
201 |
202 | ## Compliance Status
203 |
204 | ### OWASP Top 10 2021:
205 | - A06:2021 (Vulnerable and Outdated Components): FIXED (Composer deps)
206 | - A03:2021 (Injection): DOCUMENTED (XSS vulnerabilities)
207 | - A05:2021 (Security Misconfiguration): DOCUMENTED (postMessage)
208 | - A08:2021 (Software Integrity Failures): DOCUMENTED (dynamic script loading)
209 |
210 | ### Security Standards:
211 | - Dependency vulnerabilities: RESOLVED
212 | - Code vulnerabilities: DOCUMENTED with remediation guidance
213 | - All findings mapped to CWE classifications
214 |
--------------------------------------------------------------------------------
/src/js/adapter/googledrive/index.js:
--------------------------------------------------------------------------------
1 | var auth2, numInstances;
2 |
3 | module.exports = {
4 | translations: {
5 | description: {
6 | en: 'Google Drive ({{config.email || "Not connected"}})',
7 | de: 'Google Drive ({{config.email || "Nicht verbunden"}})'
8 | },
9 | 'document': {
10 | en: 'Doc',
11 | de: 'Doc'
12 | },
13 | 'spreadsheet': {
14 | en: 'Spreadsheet',
15 | de: 'Tabelle'
16 | },
17 | 'presentation': {
18 | en: 'Presentation',
19 | de: 'Präsentation'
20 | },
21 | 'map': {
22 | en: 'My Maps',
23 | de: 'My Maps'
24 | },
25 | 'form': {
26 | en: 'Form',
27 | de: 'Formular'
28 | },
29 | 'drawing': {
30 | en: 'Drawing',
31 | de: 'Zeichnung'
32 | },
33 | 'folder': {
34 | en: 'Folder',
35 | de: 'Ordner'
36 | },
37 | 'script': {
38 | en: 'App Script',
39 | de: 'App Script'
40 | }
41 | },
42 | http: {
43 | base: 'https://content.googleapis.com/drive/v3',
44 | http: {
45 | // Google web services by default have a limit of 1000 Requests / 100 seconds
46 | // So keep 100ms meantime between all requests
47 | throttle: 100
48 | }
49 | },
50 | created: function () {
51 | if (this.config.hosted_domain && numInstances) {
52 | require('vue').console.warn('hosted_domain is a global option for Google Auth - can not have multiple storages based on that');
53 | }
54 | if (this.auth) {
55 | this.config.email = this.auth.email;
56 | }
57 | numInstances++;
58 | },
59 | stored: {
60 | auth: true
61 | },
62 | methods: {
63 | loadAuth2: function() {
64 | return this.$promise(function(resolve) {
65 | if (auth2) {
66 | resolve();
67 | } else {
68 | this.util.loadScript('https://apis.google.com/js/platform.js', function() {
69 | gapi.load('auth2', function() {
70 | var options = {
71 | client_id: this.config.client_id,
72 | scope: 'https://www.googleapis.com/auth/drive.readonly'
73 | };
74 | if (this.config.hosted_domain) {
75 | options.hosted_domain = this.config.hosted_domain;
76 | }
77 | gapi.auth2.init(options).then(function(a) {
78 | auth2 = a;
79 | resolve();
80 | });
81 | }.bind(this));
82 | }.bind(this));
83 | }
84 | });
85 | },
86 | signIn: function () {
87 | return this.$promise(function(resolve) {
88 | this.loadAuth2().then(function() {
89 | if (auth2.isSignedIn.get()) {
90 | resolve(auth2.currentUser.get());
91 | } else {
92 | var open = this.$parent.open;
93 | this.$parent.open = true;
94 | var div = this.$el.appendChild(document.createElement('div'));
95 | div.className = 'panel panel-default';
96 | var button = div.appendChild(document.createElement('div'));
97 | button.className = 'btn btn-default btn-block';
98 | button.innerHTML = this.t('login.login');
99 |
100 | auth2.attachClickHandler(button, {}, function(currentUser) {
101 | this.$parent.open = open;
102 | this.$el.removeChild(div);
103 | resolve(currentUser);
104 | }.bind(this));
105 | }
106 | }.bind(this));
107 | });
108 | },
109 | setupToken: function() {
110 | return this.$promise(function(resolve) {
111 | var setup = function() {
112 | this.$options.http.params = {key: this.config.api_key };
113 | this.$options.http.headers = {Authorization: 'Bearer ' + this.auth.token};
114 | resolve();
115 | }.bind(this);
116 | if (this.auth && this.auth.expires_at < Date.now()) {
117 | this.auth = null;
118 | }
119 | if (this.auth) {
120 | setup();
121 | } else {
122 | this.signIn().then(function(user) {
123 | this.auth = {
124 | token: user.getAuthResponse().access_token,
125 | expires_at: user.getAuthResponse().expires_at,
126 | email: user.getBasicProfile().getEmail()
127 | };
128 | setup();
129 | }.bind(this));
130 | }
131 | });
132 | },
133 | loadItems: function() {
134 | return this.$promise(function(resolve) {
135 | this.setupToken().then(function (token) {
136 | this.http.get('files', {params: {key: this.config.api_key, q: '\'root\' in parents'}});
137 | }.bind(this));
138 | });
139 | }
140 | },
141 | events: {
142 | 'load-items': function (tree) {
143 | this.setupToken().then(function (token) {
144 | this.http.get(
145 | 'files',
146 | {
147 | params: {
148 | key: this.config.api_key,
149 | q: '\'' + (tree.item ? tree.item.id : 'root') + '\' in parents and trashed = false',
150 | fields: 'files,kind'
151 | }
152 | }
153 | ).then(function(response) {
154 | tree.items = this.sortItems(
155 | JSON.parse(response.data).files.map(function(item) {
156 | var type = item.mimeType === 'application/vnd.google-apps.folder' ? 'dir' : 'file';
157 | var typeLabel, googleType;
158 | if (item.mimeType.indexOf('/vnd.google-apps.') > 0) {
159 | googleType = item.mimeType.split('.').pop();
160 | typeLabel = 'Google ' + (this.t(googleType) || googleType[0].toUpperCase() + googleType.substr(1));
161 | }
162 | return this.createItem({
163 | id: item.id,
164 | name: item.name,
165 | type: type,
166 | mediaType: {
167 | icon: item.iconLink,
168 | iconBig: (type === 'file' && item.iconLink) ? item.iconLink.replace(/\/icon_[0-9]+_([^_]+)_[^\/]+/, '/mediatype/icon_1_$1_x128.png') : undefined,
169 | label: typeLabel
170 | },
171 | links: {
172 | download: item.webContentLink,
173 | open: item.webViewLink
174 | },
175 | extension: item.fileExtension,
176 | thumbnail: item.thumbnailLink,
177 | data: item
178 | });
179 | }.bind(this))
180 | );
181 | });
182 | }.bind(this));
183 | }
184 | }
185 | };
186 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var sass = require('gulp-sass');
3 | var uglify = require('gulp-uglify');
4 | var sourcemaps = require('gulp-sourcemaps');
5 | var browserify = require('browserify');
6 | var partialify = require('partialify');
7 | var source = require('vinyl-source-stream');
8 | var buffer = require('vinyl-buffer');
9 | var livereload = require('gulp-livereload');
10 |
11 | gulp.task('html', function() {
12 | // Touch the html file
13 | gulp.src('index.html').pipe(livereload());
14 | });
15 |
16 | gulp.task('sass', function() {
17 | return gulp.src('./src/sass/**/*.scss')
18 | .pipe(sass({functions: {
19 | 'base64Encode($string)': function (string) {
20 | var encoded = new Buffer(string.getValue()).toString('base64');
21 | return new sass.compiler.types.String(encoded);
22 | }
23 | }}).on('error', sass.logError))
24 | .pipe(gulp.dest('./dist/css'))
25 | .pipe(livereload());
26 | });
27 |
28 | var bundles = ['app', 'picker', 'adapter/entermediadb', 'adapter/github', 'adapter/googledrive', 'adapter/dummy'];
29 | bundles.forEach(function(bundle) {
30 | gulp.task('js-' + bundle, function() {
31 | browserify({
32 | entries: 'src/js/' + bundle + '/index.js',
33 | debug: true,
34 | standalone: 'AssetPicker' + (bundle !== 'picker' ? bundle.replace(/(^|\/)([a-z])/g, function (m) { return (m.length > 1 ? m[1] : m).toUpperCase() }) : '')
35 | })
36 | .transform(partialify)
37 | .bundle()
38 | .on('error', function (err) {
39 | console.log(err.toString());
40 | this.emit("end");
41 | })
42 | .pipe(source(bundle + '.js'))
43 | .pipe(buffer())
44 | .pipe(sourcemaps.init({ loadMaps: true }))
45 | .pipe(uglify())
46 | .on('error', function (err) {
47 | console.log(err.toString());
48 | this.emit("end");
49 | })
50 | .pipe(sourcemaps.write('maps'))
51 | .pipe(gulp.dest('dist/js'))
52 | .pipe(livereload());
53 | });
54 | });
55 | gulp.task('js', bundles.map(function (bundle) {
56 | return 'js-' + bundle;
57 | }));
58 |
59 | gulp.task('start-server', function() {
60 | connect.server({ root: 'dist', livereload: true });
61 | });
62 |
63 | gulp.task('release', function (cb) {
64 | const readline = require('readline'),
65 | cp = require('child_process'),
66 | fs = require('fs');
67 | var ask = function (question, options, callback) {
68 | question += ' ';
69 | if (typeof options === 'function') {
70 | callback = options;
71 | options = undefined;
72 | } else {
73 | question += '[' + options.join(',') + '] ';
74 | }
75 | var rl = readline.createInterface({
76 | input: process.stdin,
77 | output: process.stdout
78 | }),
79 | handler = function(answer) {
80 | if (options && options.indexOf(answer) === -1) {
81 | console.log('Invalid answer - please type ' + options.slice(0, -1).join(', ') + ' or ' + options.slice(0).pop());
82 | rl.question(question, handler);
83 | return;
84 | }
85 | callback(answer);
86 | rl.close();
87 | };
88 | rl.question(question, handler);
89 | },
90 | git = function (command, successCallback, errorCallback) {
91 | cp.exec('git ' + command, function(err, stdout, stderr) {
92 | if (err) {
93 | if (errorCallback) {
94 | errorCallback(stderr);
95 | } else {
96 | console.error(stderr);
97 | cb('git ' + command + ' failed');
98 | }
99 | } else if (successCallback) {
100 | successCallback(stdout.replace(/\s+$/, ''));
101 | }
102 | });
103 | };
104 | git ('status -s', function (changes) {
105 | var proceed = function () {
106 | git('tag -l', function (tags) {
107 | var tag = tags.split("\n").pop();
108 | console.log('The latest tag is ' + tag + ' - choose which should be the next:');
109 | var parts = tag.split('.'), nextTags = {};
110 | for (var i = parts.length - 1; i >= 0; i--) {
111 | var currentParts = parts.slice(0), n = parts.length - i;
112 | for (j = parts.length - 1; j > i; j--) {
113 | currentParts[j] = 0;
114 | }
115 | currentParts[i] = parseInt(parts[i]) + 1;
116 | nextTags[n] = currentParts.join('.');
117 | console.log(n + ') ' + nextTags[n]);
118 | }
119 | ask('Next version?', Object.keys(nextTags), function (nextTag) {
120 | nextTag = nextTags[nextTag];
121 | cp.exec('npm version --no-git-tag-version ' + nextTag, function (err, stdout, stderr) {
122 | if (err) {
123 | if (stderr.toString().indexOf('Version not changed') === -1) {
124 | throw stderr.toString();
125 | }
126 | }
127 | cp.exec('npm publish', function (err, stdout, stderr) {
128 | if (err) {
129 | if (stderr.toString().match(/code\s+ENEEDAUTH/)) {
130 | cb('Not authenticated to npm - run npm adduser');
131 | return;
132 | } else {
133 | throw stderr.toString();
134 | }
135 | }
136 | fs.writeFileSync(
137 | 'README.md',
138 | fs.readFileSync('README.md').toString().replace(new RegExp(tag.replace(/\./, '\\.'), 'g'), nextTag)
139 | );
140 | git('commit -m "Bumped version to ' + nextTag + '" package.json README.md', function () {
141 | git('log ' + tag + '..HEAD --format="- %s"', function (log) {
142 | fs.writeFileSync('.commit-msg', 'Tagging ' + nextTag + ":\n" + log);
143 | git('tag -a ' + nextTag + ' -F .commit-msg', function () {
144 | fs.unlinkSync('.commit-msg');
145 | git('push --follow-tags', function () {
146 | cb();
147 | });
148 | });
149 | });
150 | });
151 | });
152 | });
153 | });
154 | });
155 | };
156 | if (changes) {
157 | ask("You have uncommited changes:\n" + changes + "\nDo you want to proceed without commiting them?", ['y', 'n'], function (answer) {
158 | if (answer === 'y') {
159 | proceed();
160 | } else {
161 | cb();
162 | }
163 | });
164 | } else {
165 | proceed();
166 | }
167 | });
168 | });
169 |
170 | gulp.task('compile', ['html', 'sass', 'js']);
171 | gulp.task('watch', function () {
172 | livereload.listen();
173 | gulp.watch('index*.html', ['html']);
174 | bundles.forEach(function(bundle) {
175 | gulp.watch('src/js/' + bundle + '/**/*.*', ['js-' + bundle]);
176 | });
177 | gulp.watch('src/js/shared/**/*.*', ['js']);
178 | gulp.watch('src/sass/**/*.scss', ['sass']);
179 | });
180 | gulp.task('serve', ['watch', 'start-server']);
181 | gulp.task('default', ['compile']);
182 |
--------------------------------------------------------------------------------
/src/js/adapter/entermediadb/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | translations: {
3 | description: {
4 | en: 'EnterMediaDB on {{config.url}}',
5 | de: 'EnterMediaDB auf {{config.url}}'
6 | }
7 | },
8 | template: require('./template.html'),
9 | http: function() {
10 | var options = {
11 | base: this.config.url.replace(/\/+$/, '') + '/mediadb/services',
12 | validate: function (response) {
13 | response.data = response.json();
14 | if (response.data.response.status !== 'ok') {
15 | this.login(function(username, password, callback) {
16 | this.http.post('authentication/login', {id: username, password: password}, {validate: function (response) {
17 | response.data = response.json();
18 | response.isValid(response.data.response.status === 'ok');
19 | }}).then(
20 | function (response) {
21 | callback(response.data.results.status !== 'invalidlogin');
22 | }
23 | );
24 | }).then(response.reload);
25 | } else {
26 | response.isValid();
27 | }
28 | }
29 | };
30 | return options;
31 | },
32 | data: function () {
33 | return {
34 | category: null,
35 | search: null,
36 | items: null,
37 | results: {},
38 | extensions: null
39 | }
40 | },
41 | watch: {
42 | 'appConfig.pick': {
43 | handler: function (config) {
44 | // Reload latest items when extensions have changed
45 | var oldTerms = this.assembleTerms();
46 | this.extensions = config.extensions;
47 | var newTerms = this.assembleTerms();
48 | if (oldTerms.hash !== newTerms.hash && this.results[oldTerms.hash]) {
49 | var items = this.results[oldTerms.hash].items;
50 | while (items.length > 0) {
51 | items.pop();
52 | }
53 | this.loadAssets(items);
54 | }
55 | },
56 | immediate: true
57 | }
58 | },
59 | dateFormat: 'YYYY-MM-DDTHH:mm:ss',
60 | methods: {
61 | assembleTerms: function () {
62 | var terms = [],
63 | pushTerm = function (field, operator, value) {
64 | terms.push({field: field, operator: operator, value: value});
65 | };
66 | if (this.category) {
67 | pushTerm('category', 'exact', this.category.id);
68 | }
69 | if (this.search) {
70 | pushTerm('description', 'freeform', this.search);
71 | }
72 | if (this.extensions && this.extensions.length) {
73 | pushTerm('fileformat', 'matches', this.extensions.join('|'))
74 | }
75 | if (!terms.length) {
76 | pushTerm('id', 'matches', '*');
77 | }
78 | return terms;
79 | },
80 | loadAssets: function (items) {
81 | var terms = this.assembleTerms();
82 | var query = JSON.stringify(terms);
83 | var result = this.results[query];
84 | if (!result) {
85 | result = {page: 0, pages: 0, items: items || []};
86 | result.items.total = result.items.total || result.items.length;
87 | this.results[query] = result;
88 | } else {
89 | if (items && result.items !== items) {
90 | Array.prototype.push.apply(items, result.items);
91 | items.total = result.items.total;
92 | items.loading = result.items.loading;
93 | items.query = query;
94 | result.items = items;
95 | }
96 | if (result.page === result.pages) {
97 | return this.$promise(function (resolve) {
98 | resolve(result);
99 | });
100 | }
101 | }
102 |
103 | result.items.loading = true;
104 | result.items.query = query;
105 |
106 | return this.http.post(
107 | 'module/asset/search',
108 | {
109 | page: '' + (result.page + 1),
110 | hitsperpage: '20',
111 | query: {
112 | terms: terms
113 | }
114 | }
115 | ).then((function(response) {
116 | if (result.items.query === query) {
117 | result.page = parseInt(response.data.response.page);
118 | result.pages = parseInt(response.data.response.pages);
119 | result.items.total = parseInt(response.data.response.totalhits);
120 | result.items.loading = false;
121 | var emshareUrl = this.config.url.replace(/\/+$/, '') + '/emshare';
122 | response.data.results.forEach((function (asset) {
123 | var item = this.createItem({
124 | id: asset.id,
125 | query: query,
126 | type: asset.isfolder ? 'file' : 'dir',
127 | name: asset.assettitle || asset.name || asset.primaryfile,
128 | title: asset.assettitle,
129 | extension: asset.fileformat.id,
130 | created: this.parseDate(asset.assetcreationdate || asset.assetaddeddate),
131 | modified: this.parseDate(asset.assetmodificationdate),
132 | thumbnail: this.url(
133 | '/emshare/views/modules/asset/downloads/preview/thumb/' +
134 | encodeURI(asset.sourcepath) + '/thumb.jpg',
135 | this.config.url
136 | ),
137 | links: {
138 | open: emshareUrl + '/views/modules/asset/editor/viewer/index.html?assetid=' + asset.id,
139 | download: emshareUrl + '/views/activity/downloadassets.html?assetid=' + asset.id
140 | },
141 | data: asset
142 | });
143 | result.items.push(item);
144 | }).bind(this));
145 | }
146 | return result;
147 | }).bind(this));
148 | }
149 | },
150 | events: {
151 | 'select-item': function (item) {
152 | if (item === 'entrypoint') {
153 | this.category = null;
154 | this.search = null;
155 | this.loadAssets().then((function(response) {
156 | this.items = response.items;
157 | this.$parent.$dispatch('select-item', this);
158 | }).bind(this));
159 | } else {
160 | return true;
161 | }
162 | },
163 | 'load-more-items': function (results) {
164 | this.loadAssets(results);
165 | },
166 | 'search': function (sword, results) {
167 | this.search = sword;
168 | this.loadAssets(results);
169 | return true;
170 | },
171 | 'category-load-items': function (tree) {
172 | this.http.post(
173 | 'lists/search/category',
174 | {
175 | hitsperpage: '100',
176 | query: {
177 | terms: [
178 | {
179 | field: 'parentid',
180 | operator: 'exact',
181 | value: tree.item ? tree.item.id : 'index'
182 | }
183 | ]
184 | }
185 | }
186 | ).then(function (response) {
187 | tree.items = response.data.results.map((function(category) {
188 | return this.createItem({
189 | id: category.id,
190 | name: category.name,
191 | type: 'category',
192 | data: category
193 | });
194 | }).bind(this));
195 | });
196 | },
197 | 'category-select-item': function (tree) {
198 | this.category = tree.item;
199 | this.search = null;
200 | this.loadAssets(tree.items).then(function (response) {
201 | if (tree.selected) {
202 | this.$dispatch('select-item', tree);
203 | }
204 | }.bind(this));
205 | }
206 | }
207 | };
208 |
--------------------------------------------------------------------------------
/src/js/app/index.js:
--------------------------------------------------------------------------------
1 | var Vue = require('vue');
2 |
3 | Vue.use(require('vue-resource'));
4 |
5 | var i18nMixin = require('vue-i18n-mixin');
6 | i18nMixin.methods.t = i18nMixin.methods.translate;
7 | delete i18nMixin.methods.translate;
8 | Vue.mixin(i18nMixin);
9 |
10 | Vue.http.interceptors.push(function(options, next) {
11 | this.$root.loading++;
12 | next(function(response) {
13 | this.$root.loading--;
14 | });
15 | });
16 |
17 | var scriptURL = (function() {
18 | var scripts = document.getElementsByTagName('script');
19 | return scripts[scripts.length - 1].src;
20 | })();
21 |
22 | var Util = require('./util');
23 | require('./components/tree');
24 |
25 | var extend = require('extend');
26 |
27 | var storageComponent = require('./components/storage');
28 |
29 | module.exports = Vue.extend({
30 | template: require('./index.html'),
31 | data: function () {
32 | return {
33 | picked: require('./model/pick'),
34 | selection: require('./model/selection'),
35 | maximized: false,
36 | loading: 0, // In/decreased by http interceptor above
37 | config: undefined,
38 | loaded: false,
39 | isLogin: false
40 | }
41 | },
42 | translations: require('./locales'),
43 | computed: {
44 | numStorages: function () {
45 | return this.config && this.config.storages ? Object.keys(this.config.storages).length : 0;
46 | },
47 | locale: function () {
48 | var lang, available = ['en', 'de'];
49 | if (!this.config || this.config.language === 'auto') {
50 | lang = (navigator.language || navigator.userLanguage).replace(/^([a-z][a-z]).+$/, '$1');
51 | } else {
52 | lang = this.config.language;
53 | }
54 | if (available.indexOf(lang) < 0) {
55 | lang = available[0];
56 | if (this.config && this.config.language !== 'auto') {
57 | console.warn('Configured language %s is not available', this.config.language);
58 | }
59 | }
60 | return lang;
61 | },
62 | summary: function () {
63 | var summary = {numItems: 0, numStorages: 0},
64 | getLength = function (items) {
65 | if (items.total) {
66 | var length = items.total;
67 | for (var i = 0, l = items.length; i < l; i++) {
68 | if (items[i].query !== items.query && this.visible(items[i])) {
69 | length++;
70 | }
71 | }
72 | return length;
73 | } else {
74 | return items.filter(this.visible).length;
75 | }
76 | }.bind(this);
77 | if (this.selection.search) {
78 | for (var key in this.selection.results) {
79 | if (this.selection.results.hasOwnProperty(key)) {
80 | var l = getLength(this.selection.results[key]);
81 | summary.numItems += l;
82 | if (l > 0) {
83 | summary.numStorages++;
84 | }
85 | }
86 | }
87 | } else {
88 | summary.numItems = getLength(this.selection.items);
89 | }
90 | return summary;
91 | }
92 | },
93 | components: {
94 | storage: storageComponent,
95 | items: require('./components/items'),
96 | handle: require('./components/handle')
97 | },
98 | created: function () {
99 | if (!this.$options.messaging && window.parent && window.parent !== window) {
100 | var Messaging = require('../shared/util/messaging');
101 | this.$options.messaging = new Messaging('*', window.parent);
102 | }
103 | var config = require('./config');
104 | if (this.$options.config) {
105 | extend(true, config, this.$options.config);
106 | }
107 | if (this.$options.messaging) {
108 | this.$options.messaging.registerServer('app', this);
109 | this.callPicker('getOrigin').then(function (origin) {
110 | this.$options.messaging.origin = origin;
111 | this.callPicker('getConfig').then(function(overrideConfig) {
112 | extend(true, config, overrideConfig);
113 | this.$dispatch('config-loaded', config);
114 | }.bind(this));
115 | }.bind(this));
116 | } else {
117 | this.$dispatch('config-loaded', config);
118 | }
119 | },
120 | ready: function() {
121 | window.addEventListener('resize', function() {
122 | this.$broadcast('resize');
123 | }.bind(this));
124 | },
125 | events: {
126 | 'handle-move': function () {
127 | this.$broadcast('resize');
128 | },
129 | 'config-loaded': function (config) {
130 | Vue.config.debug = config.debug;
131 | this.$set('config', config);
132 | this.$nextTick(function () {
133 | this.loadAdapters().then(function () {
134 | this.loaded = true;
135 | this.callPicker('_trigger', 'ready');
136 | });
137 | });
138 | return true;
139 | },
140 | 'finish-pick': function () {
141 | this.pick();
142 | }
143 | },
144 | watch: {
145 | maximized: function (maximized) {
146 | this.callPicker('_trigger', 'resize', maximized);
147 | }
148 | },
149 | methods: {
150 | loadAdapters: function () {
151 | return this.$promise(function (resolve, reject) {
152 | var baseAdapter = require('./adapter/base'),
153 | loadAdapters = [],
154 | loading = 0,
155 | loaded = function () {
156 | if (loading === 0) {
157 | resolve();
158 | }
159 | };
160 | for (var storage in this.config.storages) {
161 | if (this.config.storages.hasOwnProperty(storage)) {
162 | var adapter = this.config.storages[storage].adapter;
163 | if (!adapter) {
164 | throw 'Missing adapter on storage ' + storage;
165 | }
166 | if (storageComponent.components[adapter]) {
167 | continue;
168 | }
169 | if (!this.config.adapters.hasOwnProperty(adapter)) {
170 | throw 'Adapter ' + adapter + ' is not configured';
171 | }
172 | this.config.storages[storage].description = this.config.storages[storage].description || null;
173 | if (loadAdapters.indexOf(adapter) < 0) {
174 | loadAdapters.push(adapter);
175 | loading++;
176 | (function (adapter, src) {
177 | var name = 'AssetPickerAdapter' + adapter[0].toUpperCase() + adapter.substr(1);
178 | if (!src.match(/^(https?:\/\/|\/)/)) {
179 | src = scriptURL.split('/').slice(0, -1).join('/') + '/' + src;
180 | }
181 | Util.loadScript(src, function () {
182 | if (!window[name]) {
183 | reject(name + ' could not be found');
184 | }
185 | var component = window[name];
186 | this.$options.translations[adapter] = component.translations;
187 | delete component.translations;
188 | component.extends = baseAdapter;
189 | this.$options.components.storage.component(adapter, component);
190 | loading--;
191 | loaded();
192 | }.bind(this));
193 | }.bind(this))(adapter, this.config.adapters[adapter]);
194 | }
195 | }
196 | }
197 | loaded();
198 | });
199 | },
200 | visible: function (item) {
201 | return item.type !== 'file' || this.picked.isAllowed(item);
202 | },
203 | callPicker: function(method) {
204 | if (this.$options.messaging) {
205 | var args = Array.prototype.slice.call(arguments, 0);
206 | args[0] = 'picker.' + args[0];
207 | return this.$options.messaging.call.apply(this.$options.messaging, args);
208 | }
209 | },
210 | cancel: function() {
211 | this.picked.clear();
212 | this.callPicker('modal.close');
213 | },
214 | pick: function() {
215 | this.callPicker('pick', this.picked.export());
216 | this.picked.clear();
217 | },
218 | setConfig: function (config) {
219 | this.config = extend(this.config, config);
220 | }
221 | }
222 | });
223 |
--------------------------------------------------------------------------------
/src/js/app/adapter/base.js:
--------------------------------------------------------------------------------
1 | var Vue = require('vue');
2 |
3 | var Item = require('../model/item');
4 |
5 | var extend = require('extend');
6 | var fecha = require('fecha');
7 |
8 | Vue.http.interceptors.push(function(options, next) {
9 | next(function(response) {
10 | response.options = options;
11 | });
12 | });
13 |
14 | var UrlClass = function(url) {
15 | this.raw = url;
16 | };
17 | UrlClass.prototype.toString = function () {
18 | return encodeURIComponent(this.raw);
19 | };
20 |
21 | module.exports = {
22 | template: '
',
23 | props: {
24 | config: {
25 | type: Object,
26 | required: true
27 | },
28 | fetch: Boolean,
29 | storage: {
30 | type: String,
31 | required: true
32 | }
33 | },
34 | stored: {
35 | // store a data variable on storage basis:
36 | // token: true
37 | // store something globally:
38 | // token: 'github'
39 | },
40 | data: function() {
41 | var data = {
42 | loginDone: false,
43 | currentLogin: null,
44 | appConfig: require('../config'),
45 | _lastRequestTime: null
46 | };
47 | if (!this.$options.watch) {
48 | this.$options.watch = {};
49 | }
50 | Object.keys(this.$options.stored).forEach(function(key) {
51 | var storageKey;
52 | if (this.$options.stored[key] === true) {
53 | storageKey = this.storage + '_local_' + key;
54 | } else {
55 | storageKey = this.$options.stored[key] + '_' + key;
56 | }
57 | data[key] = localStorage.getItem(storageKey);
58 | if (data[key]) {
59 | data[key] = JSON.parse(data[key]);
60 | }
61 | this.$options.watch[key] = function(data) {
62 | if (data === null) {
63 | localStorage.removeItem(storageKey)
64 | } else {
65 | localStorage.setItem(storageKey, JSON.stringify(data));
66 | }
67 | };
68 | }.bind(this));
69 | return data;
70 | },
71 | computed: {
72 | util: function() {
73 | return require('../util');
74 | },
75 | proxy: function () {
76 | if (this.config.proxy || this.appConfig.proxy.all && this.config.proxy !== false) {
77 | return (typeof this.config.proxy === 'object' ? this.config : this.appConfig).proxy;
78 | }
79 | return false;
80 | },
81 | url: function () {
82 | var proxyUrl, $proxy;
83 | if (this.proxy) {
84 | proxyUrl = this.proxy.url;
85 | $proxy = new Vue({
86 | data: { url: null }
87 | });
88 | }
89 | return function (url, base) {
90 | if (base) {
91 | url = (base + '').replace(/\/+$/, '') + '/' + (url + '').replace(/^\/+/, '');
92 | }
93 | if ($proxy) {
94 | $proxy.url = new UrlClass(url);
95 | return $proxy.$interpolate(proxyUrl);
96 | }
97 | return url;
98 | }
99 | },
100 | http: function() {
101 | if (typeof this.$options.http === 'function') {
102 | this.$options.http = this.$options.http.call(this);
103 | }
104 | if (this.config.http) {
105 | extend(true, this.$options.http, this.config.http);
106 | }
107 | var api = {},
108 | request = (function (options) {
109 | if (!options.keepUrl) {
110 | options.url = this.url(options.url, options.base);
111 | options.keepUrl = true;
112 | }
113 | if (this.proxy && this.proxy.credentials !== options.credentials) {
114 | options.credentials = this.proxy.credentials;
115 | }
116 | return this.$promise(function (resolve, reject) {
117 | var load = function() {
118 | this.$http(options).then(
119 | function(response) {
120 | if (response.options.validate) {
121 | response.reload = function () {
122 | return request(options).then(resolve, reject);
123 | };
124 | response.isValid = function (isValid) {
125 | if (isValid === false) {
126 | throw 'Invalid response';
127 | } else {
128 | resolve(response);
129 | }
130 | };
131 | response.options.validate.call(this, response, resolve);
132 | } else {
133 | resolve(response);
134 | }
135 | }.bind(this),
136 | reject
137 | );
138 | }.bind(this);
139 | if (options.throttle) {
140 | var throttle = function () {
141 | var now = Date.now();
142 | if (!this._lastRequestTime || now - options.throttle >= this._lastRequestTime) {
143 | this._lastRequestTime = now;
144 | load();
145 | } else {
146 | window.setTimeout(throttle, options.throttle - (this._lastRequestTime ? now - this._lastRequestTime : 0));
147 | }
148 | }.bind(this);
149 | throttle();
150 | } else {
151 | load();
152 | }
153 | });
154 | }).bind(this);
155 |
156 | ['get', 'delete', 'head', 'jsonp'].forEach(function(method) {
157 | api[method] = function (url, options) {
158 | options = extend({}, this.$options.http, options);
159 | options.method = method.toUpperCase();
160 | options.url = url;
161 | return request(options);
162 | }.bind(this)
163 | }.bind(this));
164 |
165 | ['post', 'put', 'patch'].forEach(function(method) {
166 | api[method] = function (url, data, options) {
167 | options = extend({}, this.$options.http, options);
168 | options.method = method.toUpperCase();
169 | options.url = url;
170 | options.body = data;
171 | return request(options);
172 | }.bind(this)
173 | }.bind(this));
174 | return api;
175 | }
176 | },
177 | dateFormat: undefined,
178 | methods: {
179 | t: function(key) {
180 | if (key.indexOf('.') < 0) {
181 | key = this.config.adapter + '.' + key;
182 | }
183 | return this.$root.t(key);
184 | },
185 | sortItems: function (items) {
186 | return items.sort(function (a, b) {
187 | if (a.type === 'dir' && b.type !== 'dir') {
188 | return -1;
189 | } else if (a.type !== 'dir' && b.type === 'dir') {
190 | return 1;
191 | }
192 | var nameA = a.name.toLowerCase(), nameB = b.name.toLowerCase();
193 | if (nameA < nameB)
194 | return -1;
195 | if (nameA > nameB)
196 | return 1;
197 | return 0;
198 | });
199 | },
200 | parseDate: function (date) {
201 | if (date) {
202 | return fecha.parse(date, this.$options.dateFormat);
203 | }
204 | },
205 | createItem: function (data) {
206 | data.storage = this.storage;
207 | return new Item(data, this.appConfig.thumbnails);
208 | },
209 | login: function(authenticate) {
210 | if (!this.currentLogin) {
211 | if (this.loginDone) {
212 | throw 'Login already done';
213 | }
214 | var open = this.$parent.open;
215 | this.$parent.open = true;
216 | var Login = this.$options.components['login'];
217 | var login = new Login({
218 | el: this.$el.appendChild(document.createElement('div')),
219 | parent: this
220 | });
221 | if (this.config.loginHint) {
222 | login.hint = this.config.loginHint;
223 | }
224 | this.currentLogin = login.login(authenticate.bind(this)).then((function () {
225 | this.loginDone = true;
226 | this.$parent.open = open;
227 | }).bind(this));
228 | }
229 | return this.currentLogin;
230 | }
231 | },
232 | components: {
233 | login: require('../components/login')
234 | }
235 | };
236 |
--------------------------------------------------------------------------------
/dist/js/maps/adapter/github.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["adapter/node_modules/browserify/node_modules/browser-pack/_prelude.js","adapter/src/js/adapter/github/index.js","adapter/github.js"],"names":["f","exports","module","define","amd","g","window","global","self","this","AssetPickerAdapterGithub","e","t","n","r","s","o","u","a","require","i","Error","code","l","call","length","1","translations","description","en","de","http","base","data","token","created","appConfig","github","localStorage","getItem","watch","setItem","removeItem","events","load-items","tree","get","config","username","repository","item","id","headers","Authorization","then","response","items","map","file","createItem","path","replace","name","type","links","open","html_url","bind","sortItems","$dispatch","createToken","methods","login","password","callback","baseUrl","document","location","protocol","host","fingerprint","options","btoa","createAuthorization","post","note","scopes"],"mappings":"CAAA,SAAAA,GAAA,GAAA,gBAAAC,UAAA,mBAAAC,QAAAA,OAAAD,QAAAD,QAAA,IAAA,kBAAAG,SAAAA,OAAAC,IAAAD,UAAAH,OAAA,CAAA,GAAAK,EAAAA,GAAA,mBAAAC,QAAAA,OAAA,mBAAAC,QAAAA,OAAA,mBAAAC,MAAAA,KAAAC,KAAAJ,EAAAK,yBAAAV,MAAA,WAAA,MAAA,SAAAW,GAAAC,EAAAC,EAAAC,GAAA,QAAAC,GAAAC,EAAAC,GAAA,IAAAJ,EAAAG,GAAA,CAAA,IAAAJ,EAAAI,GAAA,CAAA,GAAAE,GAAA,kBAAAC,UAAAA,OAAA,KAAAF,GAAAC,EAAA,MAAAA,GAAAF,GAAA,EAAA,IAAAI,EAAA,MAAAA,GAAAJ,GAAA,EAAA,IAAAhB,GAAA,GAAAqB,OAAA,uBAAAL,EAAA,IAAA,MAAAhB,GAAAsB,KAAA,mBAAAtB,EAAA,GAAAuB,GAAAV,EAAAG,IAAAf,WAAAW,GAAAI,GAAA,GAAAQ,KAAAD,EAAAtB,QAAA,SAAAU,GAAA,GAAAE,GAAAD,EAAAI,GAAA,GAAAL,EAAA,OAAAI,GAAAF,EAAAA,EAAAF,IAAAY,EAAAA,EAAAtB,QAAAU,EAAAC,EAAAC,EAAAC,GAAA,MAAAD,GAAAG,GAAAf,QAAA,IAAA,GAAAmB,GAAA,kBAAAD,UAAAA,QAAAH,EAAA,EAAAA,EAAAF,EAAAW,OAAAT,IAAAD,EAAAD,EAAAE,GAAA,OAAAD,KAAAW,GAAA,SAAAP,EAAAjB,EAAAD,GCAAC,EAAAD,SACA0B,cACAC,aACAC,GAAA,iEACAC,GAAA,oEAGAC,MACAC,KAAA,0BAEAC,KAAA,WACA,OAEAC,MAAA,OAGAC,QAAA,WACA1B,KAAAyB,MAAAzB,KAAA2B,UAAAC,OAAAH,OAAAI,aAAAC,QAAA,iBAEAC,OACAN,MAAA,SAAAA,GACAA,EACAI,aAAAG,QAAA,eAAAP,GACAI,aAAAC,QAAA,iBACAD,aAAAI,WAAA,kBAIAC,QACAC,aAAA,SAAAC,GACApC,KAAAyB,MACAzB,KAAAsB,KAAAe,IACA,SAAArC,KAAAsC,OAAAC,SAAA,IAAAvC,KAAAsC,OAAAE,WAAA,cAAAJ,EAAAK,KAAAL,EAAAK,KAAAC,GAAA,KAEAC,SACAC,cAAA,SAAA5C,KAAAyB,SAGAoB,KACA,SAAAC,GACA,GAAAC,GAAAD,EAAAtB,KAAAwB,IAAA,SAAAC,GACA,MAAAjD,MAAAkD,YACAR,GAAAO,EAAAE,KAAAC,QAAA,OAAA,IACAC,KAAAJ,EAAAI,KACAC,KAAAL,EAAAK,KACA9B,KAAAyB,EACAM,OACAC,KAAAP,EAAAQ,aAGAC,KAAA1D,MACAoC,GAAAW,MAAA/C,KAAA2D,UAAAZ,IAEA,WACA/C,KAAAyB,MAAA,KACAzB,KAAA4D,UAAA,aAAAxB,IACAsB,KAAA1D,OAGAA,KAAA6D,cAAAhB,KAAA,WACA7C,KAAA4D,UAAA,aAAAxB,IACAsB,KAAA1D,SAIA8D,SACAD,YAAA,WACA,MAAA7D,MAAA+D,MAAA,SAAAxB,EAAAyB,EAAAC,GACA,GAAAC,GAAAC,SAAAC,SAAAC,SAAA,KAAAF,SAAAC,SAAAE,KACAC,EAAA,kCAAAL,EACAM,GACA7B,SACAC,cAAA,SAAA6B,KAAAlC,EAAA,IAAAyB,KAGAU,EAAA,WACA1E,KAAAsB,KAAAqD,KACA,kBAEAC,KAAA,yBAAA5E,KAAAG,EAAA,gBAAA,OAAA+D,EACAW,QAAA,cAAA,QACAN,YAAAA,GAEAC,GACA3B,KACA,SAAAC,GAEA,GADA9C,KAAAyB,MAAAqB,EAAAtB,KAAAC,OACAzB,KAAAyB,MACA,KAAA,oCAEAwC,IAAA,IACAP,KAAA1D,QAEA0D,KAAA1D,KAEAA,MAAAsB,KAAAe,IAAA,iBAAAmC,GAAA3B,KACA,SAAAC,GACA,IAAA,GAAAnC,GAAA,EAAAG,EAAAgC,EAAAtB,KAAAR,OAAAL,EAAAG,EAAAH,IACA,GAAAmC,EAAAtB,KAAAb,GAAA4D,cAAAA,EAEA,WADAvE,MAAAsB,KAAAtB,UAAA,kBAAA8C,EAAAtB,KAAAb,GAAA+B,GAAA8B,GAAA3B,KAAA6B,EAIAA,MACAhB,KAAA1D,MACA,WACAiE,GAAA,oBCSW,IAAI","file":"adapter/github.js","sourcesContent":["(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require==\"function\"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error(\"Cannot find module '\"+o+\"'\");throw f.code=\"MODULE_NOT_FOUND\",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require==\"function\"&&require;for(var o=0;o\n \n\n'},{}],5:[function(t,e,i){var n=t("extend"),s=t("../../util"),r=t("insert-css"),o=function(){var t=document.createElement("div"),e={transition:"transitionend",OTransition:"otransitionend",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"};for(var i in e)if(e.hasOwnProperty(i)&&void 0!==t.style[i])return e[i]}(),a=function(t){var e=window.getComputedStyle(t,null),i=["transitionDuration","oTransitionDuration","MozTransitionDuration","webkitTransitionDuration"],n=i.filter(function(t){if("string"==typeof e[t]&&e[t].match(/[1-9]/))return!0});return!!n.length},c=t("../../../shared/util/messaging");e.exports=t("../../../shared/util/createClass")({construct:function(e){this.options=n({template:t("./index.html"),css:t("./index.css"),openClassName:"assetpicker-modal-open",src:null},e),this.modal=null,this.frame=null;var i=this.options.src.match(/^https?:\/\/[^\/]+/);this.messaging=new c(i?i[0]:document.location.origin)},render:function(){this.options.css&&r(this.options.css);var t=document.createElement("div");t.innerHTML=this.options.template,this.modal=t.children[0],this.modal.addEventListener("click",function(t){t.target===this.modal&&this.close()}.bind(this)),this.frame=this.modal.querySelector("iframe"),document.body.appendChild(this.modal),this._modalClass=this.modal.className},open:function(){if(!this.modal){this.render();var t=this;return this.frame.src=this.options.src,void window.setTimeout(function(){t.open()},1)}this.messaging.window=this.frame.contentWindow,s.addClass(this.modal,this.options.openClassName)},maximize:function(){s.addClass(this.modal,"assetpicker-maximized")},minimize:function(){s.removeClass(this.modal,"assetpicker-maximized")},_closed:function(){},close:function(){if(o&&a(this.modal)){var t=function(){this.modal.removeEventListener(o,t),this._closed()}.bind(this);this.modal.addEventListener(o,t)}else this._closed();s.removeClass(this.modal,this.options.openClassName)}})},{"../../../shared/util/createClass":9,"../../../shared/util/messaging":10,"../../util":8,"./index.css":3,"./index.html":4,extend:1,"insert-css":2}],6:[function(t,e,i){var n=!1,s=t("extend"),r=t("../../util");e.exports=t("../../../shared/util/createClass")({construct:function(t,e){n||r.loadCss(e.getDistUrl()+"/css/picker-ui.css"),this.config=s({unique:!0,readonly:!1},e.options.ui,{readonly:t.hasAttribute("data-ro")?["false","0"].indexOf(t.getAttribute("data-ro"))===-1:void 0,unique:t.hasAttribute("data-unique")?["false","0"].indexOf(t.getAttribute("data-unique"))===-1:void 0}),this.add=!1,this.propagate=!1,this.picker=e,this.element=t;var i=t.hasAttribute("value")?t.getAttribute("value"):void 0;if(i)try{this.picked=JSON.parse(i)}catch(o){this.picked=[],console.error("Error while parsing value of %s",t)}else this.picked=[];this.picked.constructor!==Array&&(this.picked=[this.picked]),this.render();var a=this;e.on("pick",function(e){if(!a.propagate&&this.element===t){var i=e.constructor!==Array?[e]:e;if(a.add){for(var n=0;n