├── .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 |
2 |
3 |
4 | 7 |
8 |
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 | 2 | 3 | 4 | 5 | 6 | 10 | 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 |
6 | 7 |
8 |
9 |
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 |
7 |
8 | 9 |
10 | 11 |
12 |
13 | 14 | 15 | 16 |
17 | 18 |
19 |
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 | 15 | -------------------------------------------------------------------------------- /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 | 16 |
17 | 22 | 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 | 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(' 103 | 104 | 105 | ') $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(' 130 | 131 | 132 | ') $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