├── codegen ├── .gitignore ├── run_codegen.sh ├── readme.txt └── codegen.stoneg.py ├── .eslintignore ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── question_help.md │ ├── bug_report.md │ └── feature_request.md ├── workflows │ ├── ci.yml │ ├── coverage.yml │ ├── deploy-gh-pages.yml │ └── spec_update.yml ├── pull_request_template.md └── dependabot.yml ├── test ├── .eslintrc.js └── unit │ └── utils.test.ts ├── .gitignore ├── .nycrc.json ├── .gitmodules ├── tsconfig.json ├── CODE_OF_CONDUCT.md ├── src ├── cookie.ts ├── index.html ├── team │ └── index.html ├── apicalls.ts ├── sidebar.css ├── codeview.ts ├── utils.ts └── main.ts ├── LICENSE ├── README.md ├── .eslintrc.js ├── package.json ├── gulpfile.js └── CONTRIBUTING.md /codegen/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/endpoints.ts 2 | gulpfile.js -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | /node_modules 4 | /typings 5 | /build 6 | /npm-debug.log 7 | /ReadMe.html 8 | /.idea 9 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "include": [ 4 | "src/*.ts" 5 | ], 6 | "require": ["ts-node/register"] 7 | } -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "codegen/spec"] 2 | path = codegen/spec 3 | url = https://github.com/dropbox/dropbox-api-spec.git 4 | [submodule "codegen/stone"] 5 | path = codegen/stone 6 | url = https://github.com/dropbox/stone.git 7 | -------------------------------------------------------------------------------- /codegen/run_codegen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Arun Debray 4 | # 27 July 2015 5 | 6 | # Quick wrapper for how to run my code generator 7 | # Will be edited as necessary (e.g. with the correct path) 8 | PYTHONPATH=stone python -m stone.cli -a:all codegen.stoneg.py test_output spec/*.stone 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "noImplicitAny": true, 5 | "noEmitOnError": true, 6 | "noEmit": true, 7 | "typeRoots": [ 8 | "node_modules/@types" 9 | ] 10 | }, 11 | "files": [ 12 | "src/main.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Dropbox Code Of Conduct 2 | 3 | *Dropbox believes that an inclusive development environment fosters greater technical achievement. To encourage a diverse group of contributors we've adopted this code of conduct.* 4 | 5 | Please read the Official Dropbox [Code of Conduct](https://opensource.dropbox.com/coc/) before contributing. -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | CI: 7 | continue-on-error: true 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Setup Node.js environment 12 | uses: actions/setup-node@v2.1.5 13 | with: 14 | node-version: 14 15 | - name: Install SDK 16 | run: | 17 | npm install 18 | - name: Run Linter 19 | run: | 20 | npm run lint -------------------------------------------------------------------------------- /codegen/readme.txt: -------------------------------------------------------------------------------- 1 | This directory will contain a program for generating code for the endpoints for the API Explorer. 2 | 3 | There's a minimal amount of code to actually generate, since each endpoint is wrapped up into a Typescript object, 4 | and the API Explorer uses the object's properties; thus, all we must do is generate the constructors for the objects 5 | themselves. 6 | 7 | I'm planning on using our existing utilities for working with Babel files, which means I'll probably also have to 8 | figure out how to put the API Explorer into the main codebase at the same time. 9 | -------------------------------------------------------------------------------- /test/unit/utils.test.ts: -------------------------------------------------------------------------------- 1 | import * as Utils from '../../src/utils'; 2 | 3 | describe('Endpoint Tests', () => { 4 | it('Can be constructed', (done) => { 5 | const test_endpoint = new Utils.Endpoint('users', 'get_current_account', 6 | { 7 | allow_app_folder_app: 'True', 8 | is_cloud_doc_auth: 'False', 9 | is_preview: 'False', 10 | select_admin_mode: 'whole_team', 11 | style: 'rpc', 12 | auth: 'user', 13 | host: 'api', 14 | scope: 'account_info.read', 15 | }); 16 | done(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ## **Checklist** 6 | 7 | 8 | **General Contributing** 9 | - [ ] Have you read the Code of Conduct and signed the [CLA](https://opensource.dropbox.com/cla/)? 10 | 11 | **Is This a Code Change?** 12 | - [ ] Non-code related change (markdown/git settings etc) 13 | - [ ] Code Change 14 | - [ ] Example/Test Code Change 15 | 16 | **Validation** 17 | - [ ] Does `npm test` pass? 18 | - [ ] Does `npm lint` pass? 19 | - [ ] Have you tested this change locally? -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question_help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4AC Questions / Help" 3 | about: Get help with issues you are experiencing 4 | title: '' 5 | labels: help-wanted, question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Before you start** 11 | Have you checked StackOverflow, previous issues, and Dropbox Developer Forums for help? 12 | 13 | **What is your question?** 14 | A clear and concise description the question. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your question. 18 | 19 | **Versions** 20 | * What platform/browser are you using? (if applicable) 21 | 22 | **Additional context** 23 | Add any other context about the question here. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "monthly" 17 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: CodeCov 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | schedule: 8 | - cron: 0 0 * * * 9 | 10 | jobs: 11 | Unit: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup Node.js environment 16 | uses: actions/setup-node@v2.1.5 17 | with: 18 | node-version: '14' 19 | - name: Install SDK 20 | run: | 21 | npm install 22 | - name: Generate Unit Test Coverage 23 | run: | 24 | npm run coverage:unit 25 | - name: Publish Coverage 26 | uses: codecov/codecov-action@v1.3.2 27 | with: 28 | flags: unit 29 | fail_ci_if_error: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Create a report to help us improve the API Explorer 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of the bug. 12 | 13 | **To Reproduce** 14 | The steps to reproduce the behavior 15 | 16 | **Expected Behavior** 17 | A clear description of what you expected to happen. 18 | 19 | **Actual Behavior** 20 | A clear description of what actually happened 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Versions** 26 | * What platform/browser are you using? (if applicable) 27 | 28 | **Additional context** 29 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature Request" 3 | about: Suggest an idea for the API Explorer 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Why is this feature valuable to you? Does it solve a problem you're having?** 11 | A clear and concise description of why this feature is valuable. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. (if applicable) 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/cookie.ts: -------------------------------------------------------------------------------- 1 | /* The files contains helper functions to interact with cookie storage. This will be 2 | used a fallback when session/local storage is not allowed (safari private browsing 3 | mode etc.) 4 | */ 5 | 6 | type StringMap = { [index: string]: string }; 7 | 8 | export const setItem = (key: string, item: string): void => { 9 | document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(item)}`; 10 | }; 11 | 12 | export const getItem = (key: string) : any => { 13 | const dict = getAll(); 14 | return dict[key]; 15 | }; 16 | 17 | export const getAll = (): StringMap => { 18 | const dict : StringMap = {}; 19 | const cookies: string[] = document.cookie.split('; '); 20 | 21 | cookies.forEach((value) => { 22 | if (value.length > 0) { 23 | const items: string[] = value.split('='); 24 | dict[decodeURIComponent(items[0])] = decodeURIComponent(items[1]); 25 | } 26 | }); 27 | 28 | return dict; 29 | }; 30 | -------------------------------------------------------------------------------- /.github/workflows/deploy-gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy To Github Pages 2 | on: 3 | push: 4 | branches: [ main ] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 🛎️ 10 | uses: actions/checkout@v2 11 | with: 12 | persist-credentials: false 13 | submodules: 'recursive' 14 | - name: Setup Python 15 | uses: actions/setup-python@v2.2.2 16 | with: 17 | python-version: 3.7 18 | - name: Install Packages 19 | run: | 20 | cd codegen/stone 21 | python setup.py install 22 | cd .. 23 | npm install 24 | ./run_codegen.sh 25 | cd .. 26 | npm run build 27 | - name: Deploy 🚀 28 | uses: JamesIves/github-pages-deploy-action@releases/v3 29 | with: 30 | ACCESS_TOKEN: ${{ secrets.GH_PAGES_PUBLISH_TOKEN }} 31 | BRANCH: gh-pages # The branch the action should deploy to. 32 | FOLDER: build # The folder the action should deploy. 33 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dropbox API Explorer 6 | 7 | 8 | 9 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/team/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dropbox API Explorer 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Dropbox Inc., http://www.dropbox.com/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Logo][logo]][repo] 2 | 3 | [![codecov](https://codecov.io/gh/dropbox/dropbox-api-v2-explorer/branch/main/graph/badge.svg)](https://codecov.io/gh/dropbox/dropbox-api-v2-explorer) 4 | 5 | The offical API Explorer for Dropbox API v2. 6 | 7 | API v2 Explorer is a client-side web app that lets you explore the different Dropbox APIs hosted via github pages. 8 | 9 | Live Link: https://dropbox.github.io/dropbox-api-v2-explorer/ 10 | 11 | You can view documentation for all of the APIs in our documentation: https://www.dropbox.com/developers/documentation/http/documentation 12 | 13 | ## Getting Help 14 | 15 | If you find a bug, please see [CONTRIBUTING.md][contributing] for information on how to report it. 16 | 17 | If you need help that is not specific to this SDK, please reach out to [Dropbox Support][support]. 18 | 19 | ## License 20 | 21 | This Explorer is distributed under the MIT license, please see [LICENSE][license] for more information. 22 | 23 | [logo]: https://cfl.dropboxstatic.com/static/images/sdk/api_v2_explorer_banner.png 24 | [repo]: https://github.com/dropbox/dropbox-api-v2-explorer 25 | [license]: https://github.com/dropbox/dropbox-api-v2-explorer/blob/main/LICENSE 26 | [contributing]: https://github.com/dropbox/dropbox-api-v2-explorer/blob/main/CONTRIBUTING.md 27 | [support]: https://www.dropbox.com/developers/contact 28 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | env: { 4 | node: false, 5 | es6: true, 6 | }, 7 | rules: { 8 | quotes: [2, 'single'], 9 | camelcase: 0, 10 | 'max-classes-per-file': 0, 11 | 'import/extensions': 0, 12 | 'prefer-destructuring': ['error', {'object': false, 'array': false}], 13 | 'no-useless-escape': 0, 14 | '@typescript-eslint/explicit-module-boundary-types': 0, 15 | // Added to get linter initially working, none of the below should be kept 16 | '@typescript-eslint/no-explicit-any': 0, 17 | '@typescript-eslint/no-unused-vars': 0, 18 | 'no-plusplus': 0, 19 | 'no-constant-condition': 0, 20 | 'import/no-unresolved': 0, 21 | 'no-useless-constructor': 0, 22 | 'no-use-before-define': 0, 23 | eqeqeq: 0, 24 | 'no-underscore-dangle': 0, 25 | 'no-param-reassign': 0, 26 | 'no-undef': 0, 27 | 'func-names': 0, 28 | 'vars-on-top': 0, 29 | 'no-var': 0, 30 | 'no-shadow': 0, 31 | 'no-multi-assign': 0, 32 | 'no-empty': 0, 33 | 'no-labels': 0, 34 | 'block-scoped-var': 0, 35 | 'no-lone-blocks': 0, 36 | 'no-return-assign': 0, 37 | 'no-restricted-syntax': 0, 38 | 'guard-for-in': 0, 39 | '@typescript-eslint/ban-types': 0 40 | }, 41 | plugins: ['@typescript-eslint'], 42 | extends: [ 43 | 'airbnb-base', 44 | 'plugin:@typescript-eslint/recommended', 45 | ], 46 | }; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dropbox-api-v2-explorer", 3 | "private": true, 4 | "scripts": { 5 | "build": "gulp", 6 | "watch": "gulp watch", 7 | "lint": "eslint --ext .js,.jsx,.ts src/ test/", 8 | "lint-fix": "eslint --fix --ext .js,.jsx,.ts src/ test/", 9 | "test:unit": "mocha -r ts-node/register test/unit/*.test.ts", 10 | "coverage:unit": "nyc --reporter=lcov npm run test:unit", 11 | "report": "nyc report --reporter=lcov --reporter=text" 12 | }, 13 | "devDependencies": { 14 | "@types/chai": "^4.2.16", 15 | "@types/highlight.js": "^10.1.0", 16 | "@types/mocha": "^8.2.0", 17 | "@types/react": "^17.0.3", 18 | "@types/react-dom": "*", 19 | "@typescript-eslint/eslint-plugin": "^4.0.0", 20 | "@typescript-eslint/parser": "^3.6.1", 21 | "browserify": "^17.0.0", 22 | "eslint": "^7.19.0", 23 | "eslint-config-airbnb-base": "^14.2.1", 24 | "eslint-plugin-import": "^2.22.1", 25 | "chai": "^4.3.0", 26 | "gulp": "^4.0.2", 27 | "gulp-connect": "^5.7.0", 28 | "gulp-sourcemaps": "^3.0.0", 29 | "highlight.js": "^10.6.0", 30 | "mocha": "^8.3.2", 31 | "nyc": "^15.1.0", 32 | "react": "^17.0.1", 33 | "react-dom": "^17.0.1", 34 | "ts-node": "^9.1.1", 35 | "tsify": "^5.0.2", 36 | "typescript": "^4.2.4", 37 | "vinyl": "^2.2.1", 38 | "vinyl-buffer": "^1.0.1", 39 | "vinyl-source-stream": "^2.0.0" 40 | }, 41 | "dependencies": { 42 | "react": "^17.0.1", 43 | "react-dom": "^17.0.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const browserify = require('browserify'); 3 | const source = require('vinyl-source-stream'); 4 | const tsify = require('tsify'); 5 | const connect = require('gulp-connect'); 6 | const sourcemaps = require('gulp-sourcemaps'); 7 | const buffer = require('vinyl-buffer'); 8 | 9 | const tsPath = '+(src)/**/*.ts'; 10 | const staticPath = 'src/**/*.+(html|css|jpeg)'; 11 | 12 | gulp.task('build-ts', () => { 13 | const b = browserify({ debug: true }) 14 | .add('src/main.ts') 15 | .plugin('tsify', { 16 | typescript: require('typescript'), 17 | // Use our version of typescript instead of the one specified by tsify's own dependencies. 18 | sortOutput: true, 19 | noEmitOnError: true, 20 | }) 21 | .bundle(); 22 | b.on('error', (error) => { console.log(error.toString()); b.emit('end'); }); 23 | 24 | return b 25 | .pipe(source('all.js')) 26 | .pipe(buffer()) 27 | .pipe(sourcemaps.init({ loadMaps: true })) 28 | .pipe(sourcemaps.write('./')) 29 | .pipe(gulp.dest('build')) 30 | .pipe(connect.reload()); 31 | }); 32 | 33 | gulp.task('build-static', () => gulp.src(staticPath) 34 | .pipe(gulp.dest('build')) 35 | .pipe(connect.reload())); 36 | 37 | gulp.task('default', gulp.series('build-ts', 'build-static')); 38 | 39 | gulp.task('watch', gulp.series('build-ts', 'build-static', () => { 40 | connect.server({ 41 | root: 'build', 42 | port: 8042, 43 | livereload: true, 44 | }); 45 | gulp.watch(tsPath, gulp.series('build-ts')); 46 | gulp.watch(staticPath, gulp.series('build-static')); 47 | })); 48 | -------------------------------------------------------------------------------- /.github/workflows/spec_update.yml: -------------------------------------------------------------------------------- 1 | name: Spec Update 2 | on: 3 | workflow_dispatch: 4 | repository_dispatch: 5 | types: [spec_update] 6 | 7 | jobs: 8 | Update: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Setup Python environment 13 | uses: actions/setup-python@v2.2.2 14 | with: 15 | python-version: 3.7 16 | - name: Setup Node.JS environment 17 | uses: actions/setup-node@v2.1.5 18 | with: 19 | node-version: 14 20 | - name: Get current time 21 | uses: 1466587594/get-current-time@v2 22 | id: current-time 23 | with: 24 | format: YYYY_MM_DD 25 | utcOffset: "-08:00" 26 | - name: Install SDK 27 | run: | 28 | npm install 29 | - name: Update Modules 30 | run: | 31 | git submodule init 32 | git submodule update --remote --recursive 33 | - name: Generate Branch Name 34 | id: git-branch 35 | run: | 36 | echo "::set-output name=branch::spec_update_${{ steps.current-time.outputs.formattedTime }}" 37 | - name: Generate Num Diffs 38 | id: git-diff-num 39 | run: | 40 | cd codegen/ 41 | diffs=$(git diff --submodule spec | grep ">" | wc -l) 42 | echo "Number of Spec diffs: $diffs" 43 | echo "::set-output name=num-diff::$diffs" 44 | cd .. 45 | - name: Generate Diff 46 | id: git-diff 47 | run: | 48 | cd codegen/spec 49 | gitdiff=$(git log -n ${{ steps.git-diff-num.outputs.num-diff }} --pretty="format:%n %H %n%n %b") 50 | commit="Automated Spec Update $gitdiff" 51 | commit="${commit//'%'/'%25'}" 52 | commit="${commit//$'\n'/'%0A'}" 53 | commit="${commit//$'\r'/'%0D'}" 54 | echo "Commit Message: $commit" 55 | echo "::set-output name=commit::$commit" 56 | cd ../.. 57 | - name: Generate New Routes 58 | run: | 59 | cd codegen/stone 60 | python setup.py install 61 | cd .. 62 | sh ./run_codegen.sh 63 | cd .. 64 | - name: Create Pull Request 65 | uses: peter-evans/create-pull-request@v3.8.2 66 | if: steps.git-diff-num.outputs.num-diff != 0 67 | with: 68 | token: ${{ secrets.SPEC_UPDATE_TOKEN }} 69 | commit-message: | 70 | ${{ steps.git-diff.outputs.commit}} 71 | branch: ${{ steps.git-branch.outputs.branch }} 72 | delete-branch: true 73 | title: 'Automated Spec Update' 74 | body: | 75 | ${{ steps.git-diff.outputs.commit}} 76 | base: 'main' 77 | team-reviewers: | 78 | owners 79 | maintainers 80 | draft: false -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Dropbox API V2 Explorer 2 | We value and rely on the feedback from our community. This comes in the form of bug reports, feature requests, and general guidance. We welcome your issues and pull requests and try our hardest to be timely in both response and resolution. Please read through this document before submitting issues or pull requests to ensure we have the necessary information to help you resolve your issue. 3 | 4 | ## Filing Bug Reports 5 | You can file a bug report on the [GitHub Issues][issues] page. 6 | 7 | 1. Search through existing issues to ensure that your issue has not been reported. If it is a common issue, there is likely already an issue. 8 | 9 | 2. Please ensure you are using the latest version of the SDK. While this may be a valid issue, we only will fix bugs affecting the latest version and your bug may have been fixed in a newer version. 10 | 11 | 3. Provide as much information as you can regarding the language version, SDK version, and any other relevant information about your environment so we can help resolve the issue as quickly as possible. 12 | 13 | ## Submitting Pull Requests 14 | 15 | We are more than happy to recieve pull requests helping us improve the state of our SDK. You can open a new pull request on the [GitHub Pull Requests][pr] page. 16 | 17 | 1. Please ensure that you have read the [License][license], [Code of Conduct][coc] and have signed the [Contributing License Agreement (CLA)][cla]. 18 | 19 | 2. Please add tests confirming the new functionality works. Pull requests will not be merged without passing continuous integration tests unless the pull requests aims to fix existing issues with these tests. 20 | 21 | ## Updating Generated Code 22 | 23 | Generated code can be updated by running the following code: 24 | 25 | ``` 26 | $ npm install 27 | $ git submodule init 28 | $ git submodule update --remote --recursive 29 | $ cd codegen/stone 30 | $ python setup.py install 31 | $ cd .. 32 | $ sh ./run_codegen.sh 33 | ``` 34 | 35 | ## Testing the Code 36 | 37 | Tests live under the test/ folder and are then broken down into the type of test it is. To run both the unit tests and the typescript tests, you can use: 38 | 39 | ``` 40 | $ npm test 41 | ``` 42 | 43 | To test the build of the webpage, you can use: 44 | 45 | ``` 46 | $ npm run build 47 | ``` 48 | 49 | This builds the site once and places the output into the `build/` directory. 50 | 51 | ## Running the Explorer Locally 52 | 53 | You can run the explorer locally and see changes reflected live as you develop by starting up `Gulp`: 54 | 55 | ``` 56 | $ npm run watch 57 | ``` 58 | 59 | [issues]: https://github.com/dropbox/dropbox-api-v2-explorer/issues 60 | [pr]: https://github.com/dropbox/dropbox-api-v2-explorer/pulls 61 | [coc]: https://github.com/dropbox/dropbox-api-v2-explorer/blob/main/CODE_OF_CONDUCT.md 62 | [license]: https://github.com/dropbox/dropbox-api-v2-explorer/blob/main/LICENSE 63 | [cla]: https://opensource.dropbox.com/cla/ -------------------------------------------------------------------------------- /src/apicalls.ts: -------------------------------------------------------------------------------- 1 | /* This file contains a module for functions that make calls to the API and their associated 2 | helper functions. 3 | */ 4 | 5 | import * as react from 'react'; 6 | import * as utils from './utils'; 7 | 8 | export type Callback = (component: react.Component, req: XMLHttpRequest) => void; 9 | 10 | /* Listener functions for the API calls; since downloads have a non-JSON response, they need a 11 | separate listener. 12 | */ 13 | const JSONListener: Callback = (component, resp) => { 14 | const response: string = resp.responseText; 15 | if (resp.status !== 200) { 16 | component.setState({ responseText: utils.errorHandler(resp.status, response) }); 17 | } else { 18 | component.setState({ responseText: utils.prettyJson(response) }); 19 | } 20 | }; 21 | 22 | const DownloadCallListener = (component: any, resp: XMLHttpRequest, path: string) => { 23 | if (resp.status !== 200) { 24 | component.setState({ 25 | responseText: 26 | utils.errorHandler(resp.status, utils.arrayBufToString(resp.response)), 27 | }); 28 | } else { 29 | const response: string = resp.getResponseHeader('dropbox-api-result'); 30 | component.setState({ responseText: utils.prettyJson(response) }); 31 | 32 | const toDownload: Blob = new Blob([resp.response], { type: 'application/octet-stream' }); 33 | component.setState({ 34 | downloadURL: URL.createObjectURL(toDownload), 35 | downloadFilename: path, 36 | }); 37 | } 38 | }; 39 | 40 | /* Utility for determining the correct callback function given an endpoint's kind 41 | Since the download listener needs to know the filename (for saving the file), it's 42 | passed through this function. 43 | */ 44 | export const chooseCallback = (k: utils.EndpointKind, path: string): Callback => { 45 | switch (k) { 46 | case utils.EndpointKind.Download: 47 | return (component, resp) => DownloadCallListener(component, resp, path); 48 | default: return JSONListener; 49 | } 50 | }; 51 | 52 | const initRequest = (endpt: utils.Endpoint, token: string, data: string, 53 | customHeaders: utils.Header[], listener: Callback, 54 | component: any): XMLHttpRequest => { 55 | const request = new XMLHttpRequest(); 56 | request.onload = (_: Event) => listener(component, request); 57 | request.open('POST', endpt.getURL(), true); 58 | const headers = utils.getHeaders(endpt, token, customHeaders, data); 59 | for (const key in headers) { 60 | let value: any = headers[key]; 61 | 62 | if (key == 'Content-Type' && endpt.getEndpointKind() == utils.EndpointKind.RPCLike) { 63 | value = 'text/plain; charset=dropbox-cors-hack'; 64 | } 65 | 66 | request.setRequestHeader(key, value); 67 | } 68 | return request; 69 | }; 70 | 71 | const beginRequest = (component: any) => { 72 | component.setState({ inProgress: true }); 73 | component.setState({ hideResponse: true }); 74 | }; 75 | 76 | const endRequest = (component: any) => { 77 | component.setState({ inProgress: false }); 78 | component.setState({ hideResponse: false }); 79 | }; 80 | 81 | /* This function actually makes the API call. There are three different paths, based on whether 82 | the endpoint is upload-like, download-like, or RPC-like. 83 | The file parameter will be null unless the user specified a file on an upload-like endpoint. 84 | */ 85 | const utf8Encode = (data: string, request: XMLHttpRequest) => { 86 | const blob: Blob = new Blob([data]); 87 | const reader: FileReader = new FileReader(); 88 | reader.onloadend = () => { 89 | let sendable_blob: Uint8Array = null; 90 | if (reader.result instanceof ArrayBuffer) { 91 | sendable_blob = new Uint8Array(reader.result); 92 | } else { 93 | sendable_blob = new TextEncoder().encode(reader.result); 94 | } 95 | request.send(sendable_blob); 96 | }; 97 | reader.readAsArrayBuffer(blob); 98 | }; 99 | 100 | export const APIWrapper = (data: string, endpt: utils.Endpoint, token: string, 101 | headers: utils.Header[], listener: Callback, 102 | component: any, file: File): void => { 103 | beginRequest(component); 104 | 105 | const listener_wrapper: Callback = (component, resp) => { 106 | endRequest(component); 107 | listener(component, resp); 108 | }; 109 | 110 | switch (endpt.getEndpointKind()) { 111 | case utils.EndpointKind.RPCLike: 112 | var request = initRequest(endpt, token, data, headers, listener_wrapper, component); 113 | utf8Encode(data, request); 114 | break; 115 | case utils.EndpointKind.Upload: 116 | var request = initRequest(endpt, token, data, headers, listener_wrapper, component); 117 | if (file !== null) { 118 | const reader = new FileReader(); 119 | reader.onload = () => request.send(reader.result); 120 | reader.readAsArrayBuffer(file); 121 | } else { 122 | request.send(); 123 | } 124 | break; 125 | case utils.EndpointKind.Download: 126 | var request = initRequest(endpt, token, data, headers, listener_wrapper, component); 127 | // Binary files shouldn't be accessed as strings 128 | request.responseType = 'arraybuffer'; 129 | request.send(); 130 | break; 131 | default: 132 | throw new Error('Invalid Endpoint Type'); 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /src/sidebar.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Open+Sans:100,200,300,400,600,700&subset=latin,latin-ext"); 2 | 3 | body, 4 | input, 5 | table, 6 | textarea, 7 | select, 8 | button, 9 | .normal { 10 | font-family: "Open Sans", "lucida grande", "Segoe UI", arial, verdana, "lucida sans unicode", tahoma, sans-serif; 11 | font-size: 12.5px; 12 | color: #3D464D; 13 | font-weight: normal; 14 | } 15 | 16 | input { 17 | height: 28px; 18 | border-radius: 3px; 19 | box-shadow: 0px 3px 3px #eee; 20 | border: 1px solid #bfbfbf; 21 | color: #444; 22 | padding: 4px; 23 | } 24 | 25 | input, select { 26 | margin-right: 5px; 27 | } 28 | 29 | body { 30 | background-color: #fff; 31 | min-height: 100%; 32 | margin: auto; 33 | padding-top: 20px; 34 | width: 1310px; 35 | } 36 | 37 | a, a * { 38 | cursor: pointer; 39 | outline: none; 40 | } 41 | 42 | a { 43 | color: #2895F1; 44 | text-decoration: none; 45 | } 46 | 47 | p, h1, h2, h3, h4, h5 { 48 | margin: 0 0 1em 0; 49 | line-height: 1.6em; 50 | } 51 | 52 | h1 { 53 | font-size: 18pt; 54 | font-weight: normal; 55 | margin-bottom: 30px; 56 | } 57 | 58 | h2 { 59 | padding-top: 3px; 60 | padding-bottom: 10px; 61 | margin-bottom: 4px; 62 | font-size: 10pt; 63 | } 64 | 65 | h3 { 66 | padding: 0; 67 | margin: 0; 68 | font-size: 10pt; 69 | } 70 | 71 | h4 { 72 | margin: 0 0 0.5em 0; 73 | font-weight: bold; 74 | font-size: 14px; 75 | } 76 | 77 | h5 { 78 | margin: 0 0 0.5em 0; 79 | font-weight: bold; 80 | font-size: 12px; 81 | } 82 | 83 | button { 84 | margin-right: 5px; 85 | border-radius: 3px; 86 | padding: 4px 10px 4px; 87 | text-align: center; 88 | font-weight: 600; 89 | cursor: pointer; 90 | overflow: visible; 91 | text-decoration: none; 92 | background: #007ee5; 93 | background: -o-linear-gradient(top, #168add 0%,#007ee5 100%); 94 | background: -ms-linear-gradient(top, #168add 0%,#007ee5 100%); 95 | background: -moz-linear-gradient(top, #168add 0%,#007ee5 100%); 96 | background: -webkit-linear-gradient(top, #168add 0%,#007ee5 100%); 97 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #168add), color-stop(100%, #007ee5)); 98 | background: linear-gradient(to bottom, #168add 0%,#007ee5 100%); 99 | border: 1px solid #0c6ebe; 100 | color: white; 101 | } 102 | 103 | button:hover { 104 | background: #007ee5; 105 | background: -o-linear-gradient(top, #168eef 0%,#007ee5 100%); 106 | background: -ms-linear-gradient(top, #168eef 0%,#007ee5 100%); 107 | background: -moz-linear-gradient(top, #168eef 0%,#007ee5 100%); 108 | background: -webkit-linear-gradient(top, #168eef 0%,#007ee5 100%); 109 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #168eef), color-stop(100%, #007ee5)); 110 | background: linear-gradient(to bottom, #168eef 0%,#007ee5 100%); 111 | } 112 | 113 | button:disabled { 114 | opacity: 0.4; 115 | cursor: default; 116 | } 117 | 118 | form { 119 | padding: 0; 120 | margin: 0; 121 | } 122 | 123 | label { 124 | cursor: default; 125 | } 126 | 127 | noscript { 128 | font-size: 13px; 129 | font-weight: normal; 130 | } 131 | 132 | pre { 133 | float: left; 134 | border: 1px solid #D0D4D9; 135 | border-radius: 3px; 136 | white-space: pre-wrap; 137 | } 138 | 139 | #sidebar { 140 | top:0; 141 | bottom:0; 142 | left:0; 143 | width: 250px; 144 | height: 1000px; 145 | float: left; 146 | overflow: hidden; 147 | padding-right: 40px; 148 | padding-bottom: 100px; 149 | } 150 | 151 | P.blocktext { 152 | margin-left: auto; 153 | margin-right: auto; 154 | width: 125px; 155 | } 156 | 157 | #request-container, #response-container { 158 | overflow: hidden; 159 | } 160 | 161 | #code-area{ 162 | margin-right: 10%; 163 | margin-top: 0%; 164 | } 165 | 166 | 167 | #request-area, #response-area { 168 | float: left; 169 | width: 1000px; 170 | } 171 | 172 | #page-content { 173 | padding-left: 240px; 174 | } 175 | 176 | #endpoint-list { 177 | height: inherit; 178 | overflow: hidden; 179 | 180 | margin-left: 25px; 181 | margin-top: 30px; 182 | } 183 | 184 | #endpoint-list:hover { 185 | overflow-y: auto; 186 | } 187 | 188 | #endpoint-list li { 189 | margin-top: 10px; 190 | list-style: none; 191 | } 192 | 193 | #endpoint-list li:first-child { 194 | margin-top: 20px; 195 | margin-bottom: 10px; 196 | } 197 | 198 | table { 199 | border-collapse: collapse; 200 | border-spacing: 0; 201 | } 202 | 203 | .page-table { 204 | width: 100%; 205 | } 206 | 207 | .page-table td { 208 | vertical-align: top; 209 | padding-top: 15px; 210 | padding-bottom: 15px; 211 | } 212 | 213 | .page-table tr { 214 | border-bottom: 1px solid #d3e9fb; 215 | } 216 | 217 | .page-table .label { 218 | color: #8a8a8a; 219 | text-align: left; 220 | width: 150px; 221 | } 222 | 223 | .page-table .detail-text { 224 | line-height: 19px; 225 | color: #8a8a8a; 226 | margin: 0 2px; 227 | max-width: 600px; 228 | } 229 | 230 | .page-table .text { 231 | padding: 5px 2px; 232 | } 233 | 234 | .page-table .align-right { 235 | display: inline-block; 236 | float: right; 237 | } 238 | 239 | #token-input { 240 | width: 460px; 241 | } 242 | 243 | #parameter-list { 244 | margin-bottom: 10px; 245 | } 246 | 247 | #parameter-list tr { 248 | border-bottom: solid 1px #e5e5e5; 249 | } 250 | 251 | #parameter-list td { 252 | vertical-align: middle; 253 | padding-right: 10px; 254 | padding-top: 10px; 255 | padding-bottom: 10px; 256 | } 257 | 258 | #parameter-list .parameter-input { 259 | height: 22px; 260 | } 261 | 262 | #parameter-list .parameter-input[type="file"] { 263 | border: none; 264 | box-shadow: none; 265 | outline: none; 266 | } 267 | 268 | #parameter-list table tr:last-child { 269 | border-bottom: 0; 270 | } 271 | 272 | #parameter-list .list-param-actions button { 273 | padding-left: 20px; 274 | padding-right: 20px; 275 | } 276 | 277 | #parameter-list .struct-param-actions button { 278 | padding-left: 20px; 279 | padding-right: 20px; 280 | } 281 | 282 | #auth-switch { 283 | float: right; 284 | padding-right: 20px; 285 | font-size: 13px; 286 | } 287 | 288 | .header-name { 289 | height: 26px 290 | } 291 | 292 | .header-value { 293 | height: 26px 294 | } 295 | -------------------------------------------------------------------------------- /codegen/codegen.stoneg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.6 2 | 3 | from __future__ import ( 4 | print_function, 5 | unicode_literals 6 | ) 7 | 8 | # somehow have to make this work. Maybe symbolic links? 9 | from stone.ir.data_types import ( 10 | is_boolean_type, 11 | is_float_type, 12 | is_integer_type, 13 | is_list_type, 14 | is_nullable_type, 15 | is_string_type, 16 | is_struct_type, 17 | is_timestamp_type, 18 | is_union_type, 19 | is_void_type 20 | ) 21 | from stone.backend import CodeBackend 22 | 23 | import copy 24 | 25 | class APIEndpointGenerator(CodeBackend): 26 | """Generates API Endpoint objects for the API explorer.""" 27 | 28 | endpoint_vars = [] 29 | 30 | def generate(self, api): 31 | with self.output_to_relative_path('../../src/endpoints.ts'): 32 | self.outputHeader() 33 | with self.indent(): 34 | for namespace in api.namespaces.values(): 35 | self.emit_namespace(namespace) 36 | self.emit() 37 | self.generate_multiline_list(self.endpoint_vars, 38 | delim=('',''), 39 | before='export const endpointList: Utils.Endpoint[] = [', 40 | after='];') 41 | self.outputFooter() 42 | 43 | def outputHeader(self): 44 | self.emit('// Automatically generated code; do not edit') 45 | self.emit() 46 | self.emit("import * as Utils from './utils';") 47 | self.emit() 48 | self.emit('module Endpoints {') 49 | 50 | def outputFooter(self): 51 | self.emit('}') 52 | self.emit() 53 | self.emit('export = Endpoints;') 54 | 55 | def emit_namespace(self, namespace): 56 | for route in namespace.routes: 57 | if not route.deprecated: # Skip deprecated route. 58 | self.emit_route(namespace, route) 59 | 60 | 61 | def emit_route(self, namespace, route): 62 | self.endpoint_vars.append(self._var_name(route, namespace)) 63 | 64 | def get_param_list(): 65 | if is_union_type(route.arg_data_type): 66 | return [self.data_type_constructor(route.arg_data_type, "''", is_root=True)] 67 | else: 68 | return list(map(self.parameter_constructor, route.arg_data_type.all_fields)) 69 | 70 | def is_empty_type(arg_type): 71 | return is_void_type(arg_type) or len(arg_type.all_fields) == 0 72 | 73 | # Right now, this is just upload_session_start 74 | if is_empty_type(route.arg_data_type) and 'style' in route.attrs and route.attrs['style'] == 'upload': 75 | self.emit('const {0} = new Utils.Endpoint("{1}", "{2}",'.format( 76 | self._var_name(route, namespace), 77 | namespace.name, 78 | self._route_name(route) 79 | )) 80 | with self.indent(): 81 | self._emit_attr_dict(route.attrs) 82 | self.emit('new Utils.FileParam()') 83 | self.emit(');') 84 | 85 | elif 'style' in route.attrs and route.attrs['style'] == 'upload': # is upload-style, not void 86 | self.emit('const {0} = new Utils.Endpoint("{1}", "{2}",'.format( 87 | self._var_name(route, namespace), 88 | namespace.name, 89 | self._route_name(route) 90 | )) 91 | 92 | with self.indent(): 93 | self._emit_attr_dict(route.attrs) 94 | self.emit('new Utils.FileParam(),') 95 | self.generate_multiline_list(get_param_list(), delim=('','')) 96 | self.emit(');') 97 | elif not is_empty_type(route.arg_data_type): # not upload style, and has params 98 | self.emit('const {0} = new Utils.Endpoint("{1}", "{2}",'.format( 99 | self._var_name(route, namespace), 100 | namespace.name, 101 | self._route_name(route) 102 | )) 103 | with self.indent(): 104 | self._emit_attr_dict(route.attrs) 105 | self.generate_multiline_list(get_param_list(), delim=('','')) 106 | self.emit(');') 107 | else: # void, but not upload_style 108 | self.emit('const {0} = new Utils.Endpoint("{1}", "{2}",'.format( 109 | self._var_name(route, namespace), 110 | namespace.name, 111 | self._route_name(route) 112 | )) 113 | with self.indent(): 114 | self._emit_attr_dict(route.attrs, True) 115 | self.emit(');') 116 | 117 | def _route_name(self, route): 118 | if route.version == 1: 119 | return route.name 120 | else: 121 | return '{}_v{}'.format(route.name, route.version) 122 | 123 | # converts route name into Typescript variable name 124 | def _var_name(self, route, namespace): 125 | route_name = self._route_name(route) 126 | return namespace.name + '_' + route_name.replace('/', '_') + '_endpt' 127 | 128 | # Emit route attrs to dict. 129 | def _emit_attr_dict(self, attrs, is_last=False): 130 | close = '}' if is_last else '},' 131 | if not attrs: 132 | self.emit('{' + close) 133 | return 134 | 135 | self.emit('{') 136 | with self.indent(): 137 | for k, v in attrs.items(): 138 | self.emit('{0}: "{1}",'.format(k, v)) 139 | self.emit(close) 140 | 141 | # Pattern-match on the type of the parameter 142 | # was_nullable indicates that this was wrapped in a nullable type. 143 | # A parameter is optional if it was nullable or it has a default value. 144 | def parameter_constructor(self, param, was_nullable=None): 145 | # TODO: we can't guarantee that param has a 'has_default' attribute 146 | has_default = getattr(param, 'has_default', False) 147 | return self.data_type_constructor(param.data_type, '"{0}"'.format(param.name), 148 | has_default=has_default, was_nullable=was_nullable) 149 | 150 | def data_type_constructor(self, data_type, name, has_default=False, was_nullable=False, is_root=False): 151 | optional = self._emit_bool(has_default or was_nullable) 152 | # Since params are reused between different endpoints, making a copy prevents 153 | # one parameter from overwriting information about another's arguments. 154 | if is_nullable_type(data_type): 155 | return self.data_type_constructor(data_type.data_type, name, was_nullable=True) 156 | if is_integer_type(data_type): 157 | return 'new Utils.IntParam({0}, {1})'.format(name, optional) 158 | elif is_float_type(data_type): 159 | return 'new Utils.FloatParam({0}, {1})'.format(name, optional) 160 | # It would be nice to separate timestamps out (e.g. a bunch of selectors!) 161 | elif is_string_type(data_type) or is_timestamp_type(data_type): 162 | return 'new Utils.TextParam({0}, {1})'.format(name, optional) 163 | elif is_boolean_type(data_type): 164 | return 'new Utils.BoolParam({0}, {1})'.format(name, optional) 165 | elif is_union_type(data_type) or is_struct_type(data_type): 166 | # would be nice to make this prettier 167 | if is_union_type(data_type): 168 | param_type = 'RootUnionParam' if is_root else 'UnionParam' 169 | else: 170 | param_type = 'StructParam' 171 | 172 | return 'new Utils.{0}({1}, {2}, {3})'.format( 173 | param_type, 174 | name, 175 | optional, 176 | '[' + ', '.join(self.parameter_constructor(field) for field in data_type.all_fields if field.name != 'other') + ']', 177 | ) 178 | elif is_void_type(data_type): 179 | return 'new Utils.VoidParam({0})'.format(name) 180 | elif is_list_type(data_type): 181 | return 'new Utils.ListParam({0}, {1}, (index: string): Utils.Parameter => {2})'.format( 182 | name, 183 | optional, 184 | self.data_type_constructor(data_type.data_type, 'index')) 185 | else: 186 | return 'null /* not implemented yet */' 187 | 188 | # emit Typescript representation of boolean 189 | def _emit_bool(self, b): 190 | return 'true' if b else 'false' 191 | -------------------------------------------------------------------------------- /src/codeview.ts: -------------------------------------------------------------------------------- 1 | /* The functions that handle the code view part of the interface: taking the input and 2 | representing it as an HTTP request or code to generate that request. 3 | */ 4 | 5 | import * as react from 'react'; 6 | import * as utils from './utils'; 7 | 8 | const ce = react.createElement; 9 | 10 | type Renderer = (endpoint: utils.Endpoint, token: string, paramVals: utils.Dict, 11 | headerVals: utils.Header[]) => react.ReactElement> 12 | type UploadRenderer = (endpoint: utils.Endpoint, token: string, paramVals: utils.Dict, 13 | headerVals: utils.Header[], file: File) => 14 | react.ReactElement> 15 | 16 | /* Something which I wish I had more time to fix: in this file, "upload" and "download" have the 17 | wrong meanings. Specifically, here, "upload" means a call with a file attached, and "download" 18 | means a non-RPC-like call without a file (e.g. empty uploads and downloads). This might be 19 | confusing. I originally did this because it makes more of a difference in what the code 20 | viewer looks like. 21 | */ 22 | 23 | // An object that handles a particular kind of code view for each kind of endpoint. 24 | interface CodeViewer { 25 | syntax: string; 26 | description: string; // text of the option in the selector 27 | 28 | /* Calls for the three kinds of endpoints: RPC-like (data neither uploaded nor downloaded), 29 | upload-like (data uploaded as the body of the request), and download-like (no data uploaded, 30 | but not RPC-like). If you upload with no data, it should thus be treated as download-like, 31 | which is a bit counterintuitive. 32 | */ 33 | renderRPCLike: Renderer; 34 | renderUploadLike: UploadRenderer; 35 | renderDownloadLike: Renderer; 36 | } 37 | 38 | const syntaxHighlight = (syntax: string, text: react.DetailedReactHTMLElement): 39 | react.ReactElement<{}> => ce(utils.Highlight, { className: syntax, children: null }, text); 40 | 41 | // Applies f to each element of the dict, and then appends the separator to all but the last result. 42 | // Subsequent list elements are separated by newlines. 43 | const joinWithNewlines = (dc: utils.Dict, f: (k: string, v: string) => string, sep = ','): 44 | react.DetailedReactHTMLElement[] => utils.Dict._map(dc, 45 | (k: string, v: string, i: number) => { 46 | const maybeSep = (i === Object.keys(dc).length - 1) 47 | ? '\n' : `${sep}\n`; 48 | return ce('span', { key: `${i}` }, f(k, v), maybeSep); 49 | }); 50 | 51 | // the minor differences between JSON and Python's notation 52 | const pythonStringify = (val: any): string => { 53 | if (val === true) { 54 | return 'True'; 55 | } if (val === false) { 56 | return 'False'; 57 | } if (val === null) { 58 | return 'None'; 59 | } 60 | return JSON.stringify(val); 61 | }; 62 | 63 | // Representation of a dict, or null if the passed-in dict is also null 64 | const dictToPython = (name: string, dc: utils.Dict): react.DetailedReactHTMLElement => ce('span', null, 65 | `${name} = `, 66 | (dc === null) 67 | ? 'None' : ce('span', null, 68 | '{\n', 69 | joinWithNewlines(dc, (k: string, v: any) => ` "${k}": ${pythonStringify(v)}`), 70 | '}'), '\n\n'); 71 | 72 | // For curl calls, we need to escape single quotes, and sometimes also double quotes. 73 | const shellEscape = (val: any, inQuotes = false): string => { 74 | const toReturn = JSON.stringify(val).replace(/'/g, '\'\\\'\''); 75 | if (inQuotes) return toReturn.replace(/\\/g, '\\\\').replace(/"/g, '\\\"'); 76 | return toReturn; 77 | }; 78 | 79 | // Generates the functions that make up the Python Requests code viewer 80 | const RequestsCodeViewer = (): CodeViewer => { 81 | const syntax = 'python'; 82 | 83 | // common among all three parts 84 | const preamble = (endpt: utils.Endpoint): react.DetailedReactHTMLElement => ce('span', null, [ 85 | 'import requests\n', 'import json\n\n', 86 | `url = "${endpt.getURL()}"\n\n`, 87 | ]); 88 | 89 | const requestsTemplate = (endpt: utils.Endpoint, headers: utils.Dict, 90 | dataReader: string|react.DetailedReactHTMLElement, call: string) => syntaxHighlight(syntax, ce('span', null, 91 | preamble(endpt), dictToPython('headers', headers), dataReader, call)); 92 | 93 | const requestsRPCLike: Renderer = (endpt, token, paramVals, headerVals) => requestsTemplate(endpt, 94 | utils.getHeaders(endpt, token, headerVals), dictToPython('data', paramVals), 95 | 'r = requests.post(url, headers=headers, data=json.dumps(data))'); 96 | 97 | const requestsUploadLike: UploadRenderer = (endpt, token, paramVals, 98 | headerVals, file) => requestsTemplate(endpt, 99 | utils.getHeaders(endpt, token, headerVals, JSON.stringify(paramVals)), 100 | `data = open(${JSON.stringify(file.name)}, "rb").read()\n\n`, 101 | 'r = requests.post(url, headers=headers, data=data)'); 102 | 103 | const requestsDownloadLike: Renderer = (endpt, token, 104 | paramVals, headerVals) => requestsTemplate(endpt, 105 | utils.getHeaders(endpt, token, headerVals, JSON.stringify(paramVals)), 106 | '', 'r = requests.post(url, headers=headers)'); 107 | 108 | return { 109 | syntax, 110 | description: 'Python request (requests library)', 111 | renderRPCLike: requestsRPCLike, 112 | renderUploadLike: requestsUploadLike, 113 | renderDownloadLike: requestsDownloadLike, 114 | }; 115 | }; 116 | 117 | // Python's httplib library (which is also the urllib backend) 118 | const HttplibCodeViewer = (): CodeViewer => { 119 | const syntax = 'python'; 120 | 121 | const preamble = ce('span', null, 122 | 'import sys\nimport json\n', 123 | 'if (3,0) <= sys.version_info < (4,0):\n', 124 | ' import http.client as httplib\n', 125 | 'elif (2,6) <= sys.version_info < (3,0):\n', 126 | ' import httplib\n\n'); 127 | 128 | const httplibTemplate = (endpt: utils.Endpoint, headers: utils.Dict, 129 | dataReader: string|react.DetailedReactHTMLElement, dataArg: string): react.ReactElement> => syntaxHighlight(syntax, ce('span', null, 130 | preamble, 131 | dictToPython('headers', headers), 132 | dataReader, 133 | `c = httplib.HTTPSConnection("${endpt.getHostname()}")\n`, 134 | `c.request("POST", "${endpt.getPathName()}", ${dataArg}, headers)\n`, 135 | 'r = c.getresponse()')); 136 | 137 | const httplibRPCLike: Renderer = (endpt, token, paramVals, headerVals) => httplibTemplate(endpt, 138 | utils.getHeaders(endpt, token, headerVals), 139 | dictToPython('params', paramVals), 'json.dumps(params)'); 140 | 141 | const httplibUploadLike: UploadRenderer = (endpt, token, paramVals, 142 | headerVals, file) => httplibTemplate(endpt, 143 | utils.getHeaders(endpt, token, headerVals, JSON.stringify(paramVals)), 144 | `data = open(${JSON.stringify(file.name)}, "rb")\n\n`, 'data'); 145 | 146 | const httplibDownloadLike: Renderer = (endpt, token, paramVals, headerVals) => httplibTemplate(endpt, utils.getHeaders(endpt, token, headerVals, JSON.stringify(paramVals)), '', '""'); 147 | 148 | return { 149 | syntax, 150 | description: 'Python request (standard library)', 151 | renderRPCLike: httplibRPCLike, 152 | renderUploadLike: httplibUploadLike, 153 | renderDownloadLike: httplibDownloadLike, 154 | }; 155 | }; 156 | 157 | const CurlCodeViewer = (): CodeViewer => { 158 | const syntax = 'bash'; 159 | const urlArea = (endpt: utils.Endpoint) => `curl -X POST ${endpt.getURL()} \\\n`; 160 | 161 | const makeHeaders = (headers: utils.Dict): react.DetailedReactHTMLElement => ce('span', null, 162 | utils.Dict._map(headers, (k: string, v: string, i: number): 163 | react.DetailedReactHTMLElement => { 164 | let sep = '\\\n'; 165 | if (i == Object.keys(headers).length - 1) sep = ''; 166 | return ce('span', { key: `${i}` }, ` --header '${k}: ${v}' ${sep}`); 167 | })); 168 | 169 | // The general model of the curl call, populated with the arguments. 170 | const curlTemplate = (endpt: utils.Endpoint, headers: utils.Dict, 171 | data: string): react.ReactElement> => syntaxHighlight(syntax, ce('span', null, urlArea(endpt), makeHeaders(headers), data)); 172 | 173 | const curlRPCLike: Renderer = (endpt, token, paramVals, headerVals) => curlTemplate(endpt, 174 | utils.getHeaders(endpt, token, headerVals), 175 | `\\\n --data '${shellEscape(paramVals)}'`); 176 | 177 | const curlUploadLike: UploadRenderer = (endpt, token, paramVals, headerVals, file) => { 178 | const headers = utils.getHeaders(endpt, token, headerVals, shellEscape(paramVals, false)); 179 | return curlTemplate(endpt, headers, 180 | `\\\n --data-binary @'${file.name.replace(/'/g, '\'\\\'\'')}'`); 181 | }; 182 | 183 | const curlDownloadLike: Renderer = (endpt, token, paramVals, headerVals) => curlTemplate(endpt, utils.getHeaders(endpt, token, headerVals, shellEscape(paramVals, false)), ''); 184 | 185 | return { 186 | syntax, 187 | description: 'curl request', 188 | renderRPCLike: curlRPCLike, 189 | renderUploadLike: curlUploadLike, 190 | renderDownloadLike: curlDownloadLike, 191 | }; 192 | }; 193 | 194 | const HTTPCodeViewer = (): CodeViewer => { 195 | const syntax = 'http'; 196 | 197 | const httpTemplate = (endpt: utils.Endpoint, headers: utils.Dict, 198 | body: string): react.ReactElement> => syntaxHighlight(syntax, ce('span', null, 199 | `POST ${endpt.getPathName()}\n`, 200 | `Host: https://${endpt.getHostname()}\n`, 201 | 'User-Agent: api-explorer-client\n', 202 | utils.Dict.map(headers, (key: string, value: string) => ce('span', { key }, 203 | `${key}: ${value}\n`)), 204 | body)); 205 | 206 | const httpRPCLike: Renderer = (endpt, token, paramVals, headerVals) => { 207 | const body = JSON.stringify(paramVals, null, 4); 208 | const headers = utils.getHeaders(endpt, token, headerVals); 209 | 210 | // TODO: figure out how to determine the UTF-8 encoded length 211 | // headers['Content-Length'] = ... 212 | 213 | return httpTemplate(endpt, headers, `\n${body}`); 214 | }; 215 | 216 | const httpUploadLike: UploadRenderer = (endpt, token, paramVals, headerVals, file) => { 217 | const headers = utils.getHeaders(endpt, token, headerVals, JSON.stringify(paramVals)); 218 | headers['Content-Length'] = file.size; 219 | return httpTemplate(endpt, headers, 220 | `\n--- (content of ${file.name} goes here) ---`); 221 | }; 222 | 223 | const httpDownloadLike: Renderer = (endpt, token, paramVals, headerVals) => { 224 | const headers = utils.getHeaders(endpt, token, headerVals, JSON.stringify(paramVals)); 225 | return httpTemplate(endpt, headers, ''); 226 | }; 227 | 228 | return { 229 | syntax, 230 | description: 'HTTP request', 231 | renderRPCLike: httpRPCLike, 232 | renderUploadLike: httpUploadLike, 233 | renderDownloadLike: httpDownloadLike, 234 | }; 235 | }; 236 | 237 | export const formats: utils.Dict = { 238 | curl: CurlCodeViewer(), 239 | requests: RequestsCodeViewer(), 240 | httplib: HttplibCodeViewer(), 241 | http: HTTPCodeViewer(), 242 | }; 243 | 244 | export const getSelector = (onChange: (e: react.FormEvent) => void): react.DetailedReactHTMLElement => ce('select', 245 | { onChange }, 246 | utils.Dict.map(formats, (key: string, cv: CodeViewer) => ce('option', { key, value: key }, cv.description))); 247 | 248 | export const render = (cv: CodeViewer, endpt: utils.Endpoint, 249 | token: string, paramVals: utils.Dict, 250 | headerVals: utils.Header[], file: File): react.ReactElement> => { 251 | if (endpt.getEndpointKind() === utils.EndpointKind.RPCLike) { 252 | return cv.renderRPCLike(endpt, token, paramVals, headerVals); 253 | } if (file !== null) { 254 | return cv.renderUploadLike(endpt, token, paramVals, headerVals, file); 255 | } 256 | return cv.renderDownloadLike(endpt, token, paramVals, headerVals); 257 | }; 258 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /* This file contains utility functions needed by the other modules. These can be grouped into the 2 | following broad categories: 3 | 4 | - Definitions of the Endpoint and Parameter classes, and the various Parameter subclasses 5 | corresponding to the different kinds of parameters 6 | - Utilities for token flow: getting and setting state, and retrieving or storing the token in 7 | session storage 8 | - Utilities for processing user input in order to submit it 9 | - A React class for highlighting the code view and response parts of the document 10 | - Functions to generate the headers for a given API call 11 | */ 12 | 13 | import * as react from 'react'; 14 | import { ReactNode } from 'react'; 15 | import * as cookie from './cookie'; 16 | 17 | type MappingFn = (key: string, value: any, i: number) => react.DetailedReactHTMLElement; 18 | 19 | const ce = react.createElement; 20 | 21 | const allowedHeaders = [ 22 | 'Dropbox-Api-Select-User', 23 | 'Dropbox-Api-Select-Admin', 24 | 'Dropbox-Api-Path-Root', 25 | ]; 26 | 27 | // This class mostly exists to help Typescript type-check my programs. 28 | export class Dict { 29 | [index: string]: any; 30 | 31 | /* Two methods for mapping through dictionaries, customized to the API Explorer's use case. 32 | - _map takes function from a key, a value, and an index to a React element, and 33 | - map is the same, but without an index. 34 | These are used, for example, to convert a dict of HTTP headers into its representation 35 | in code view. 36 | */ 37 | static _map = (dc: Dict, f: MappingFn): 38 | react.DetailedReactHTMLElement[] => Object.keys(dc) 39 | .map((key: string, i: number) => f(key, dc[key], i)); 40 | 41 | static map = (dc: Dict, f: (key: string, value: any) => any): 42 | react.DetailedReactHTMLElement[] => Object.keys(dc) 43 | .map((key: string) => f(key, dc[key])); 44 | } 45 | 46 | export class List { 47 | [index: number]: any; 48 | 49 | push = (value: any): void => this.push(value); 50 | } 51 | 52 | /* Helper class which deal with local storage. If session storage is allowed, items 53 | will be written to session storage. If session storage is disabled (e.g. safari 54 | private browsing mode), cookie storage will be used as fallback. 55 | */ 56 | export class LocalStorage { 57 | private static _is_session_storage_allowed(): boolean { 58 | const test = 'test'; 59 | try { 60 | localStorage.setItem(test, test); 61 | localStorage.removeItem(test); 62 | return true; 63 | } catch (e) { 64 | return false; 65 | } 66 | } 67 | 68 | public static setItem(key: string, data:string) : void { 69 | if (LocalStorage._is_session_storage_allowed()) { 70 | sessionStorage.setItem(key, data); 71 | } else { 72 | cookie.setItem(key, data); 73 | } 74 | } 75 | 76 | public static getItem(key: string) : any { 77 | if (LocalStorage._is_session_storage_allowed()) { 78 | return sessionStorage.getItem(key); 79 | } 80 | 81 | return cookie.getItem(key); 82 | } 83 | } 84 | 85 | /* There are three kinds of endpoints, and a lot of the program logic depends on what kind of 86 | endpoint is currently being shown. 87 | - An RPC-like endpoint involves no uploading or downloading of data; it sends a request 88 | with JSON data in the body, and receives a JSON response. Example: get_metadata 89 | - An upload-like endpoint sends file data in the body and the arguments in a header, but 90 | receives a JSON response. Example: upload 91 | - A download-style endpoint sends a request with JSON data, but receives the file in the 92 | response body. Example: get_thumbnail 93 | */ 94 | export enum EndpointKind {RPCLike, Upload, Download} 95 | 96 | export enum AuthType {None, User, Team, App} 97 | 98 | /* A class with all the information about an endpoint: its name and namespace; its kind 99 | (as listed above), and its list of parameters. The endpoints are all initialized in 100 | endpoints.ts, which is code-generated. 101 | */ 102 | export class Endpoint { 103 | name: string; 104 | 105 | ns: string; // namespace, e.g. users or files 106 | 107 | attrs: Dict; // All attributes 108 | 109 | params: Parameter[]; // the arguments to the API call 110 | 111 | constructor(ns: string, name: string, attrs: Dict, ...params: Parameter[]) { 112 | this.ns = ns; 113 | this.name = name; 114 | this.attrs = attrs; 115 | this.params = params; 116 | } 117 | 118 | getHostname = (): string => { 119 | switch (this.attrs.host) { 120 | case 'content': 121 | return 'content.dropboxapi.com'; 122 | case 'notify': 123 | return 'notify.dropboxapi.com'; 124 | default: 125 | return 'api.dropboxapi.com'; 126 | } 127 | }; 128 | 129 | getAuthType = (): AuthType => { 130 | if (this.attrs.host === 'notify') { 131 | return AuthType.None; 132 | } 133 | 134 | const { auth } = this.attrs; 135 | 136 | if (auth === 'team') { 137 | return AuthType.Team; 138 | } 139 | if (auth === 'app') { 140 | return AuthType.App; 141 | } 142 | 143 | return AuthType.User; 144 | }; 145 | 146 | getEndpointKind = (): EndpointKind => { 147 | switch (this.attrs.style) { 148 | case 'upload': 149 | return EndpointKind.Upload; 150 | case 'download': 151 | return EndpointKind.Download; 152 | default: 153 | return EndpointKind.RPCLike; 154 | } 155 | }; 156 | 157 | getPathName = (): string => `/2/${this.ns}/${this.name}`; 158 | 159 | getFullName = (): string => `${this.ns}_${this.name}`; 160 | 161 | getURL = (): string => `https://${this.getHostname()}${this.getPathName()}`; 162 | } 163 | 164 | // Class store information about request header. 165 | export class Header { 166 | name: string; 167 | 168 | value: string; 169 | 170 | constructor() { 171 | this.name = allowedHeaders[0]; 172 | this.value = ''; 173 | } 174 | 175 | asReact(onChangeHandler: (header: Header, removed: boolean)=> void): 176 | react.DetailedReactHTMLElement { 177 | const updateName = (event: react.FormEvent): void => { 178 | this.name = (event.target).value; 179 | onChangeHandler(this, false); 180 | }; 181 | 182 | const updateValue = (event: react.FormEvent): void => { 183 | this.value = (event.target).value; 184 | onChangeHandler(this, false); 185 | }; 186 | 187 | return ce('tr', null, 188 | ce('td', null, 189 | ce('select', { value: this.name, onChange: updateName, className: 'header-name' }, 190 | allowedHeaders.map((name: string) => ce('option', { key: name, value: name }, name)))), 191 | ce('td', null, 192 | ce('input', { 193 | type: 'text', 194 | className: 'header-value', 195 | onChange: updateValue, 196 | placeholder: 'Header Value', 197 | value: this.value, 198 | })), 199 | ce('td', null, 200 | ce('button', { onClick: () => onChangeHandler(this, true) }, 'Remove'))); 201 | } 202 | } 203 | 204 | /* A parameter to an API endpoint. This class is abstract, as different kinds of parameters 205 | (e.g. text, integer) will implement it differently. 206 | */ 207 | export class Parameter { 208 | name: string; 209 | 210 | optional: boolean; 211 | 212 | constructor(name: string, optional: boolean) { 213 | this.name = name; 214 | this.optional = optional; 215 | } 216 | 217 | getNameColumn = (): react.DetailedReactHTMLElement => { 218 | if (!Number.isNaN(+this.name)) { 219 | // Don't show name column for list parameter item. 220 | return null; 221 | } 222 | 223 | let displayName: string = (this.name !== '__file__') ? this.name : 'File to upload'; 224 | if (this.optional) displayName += ' (optional)'; 225 | const nameArgs: Dict = this.optional ? { style: { color: '#999' } } : {}; 226 | return ce('td', nameArgs, displayName); 227 | }; 228 | 229 | defaultValue = (): any => { 230 | if (this.optional) { 231 | return null; 232 | } 233 | 234 | return this.defaultValueRequired(); 235 | }; 236 | 237 | /* Renders the parameter's input field, using another method which depends on the 238 | parameter's subclass. 239 | */ 240 | asReact(props: Dict, key: string): react.DetailedReactHTMLElement { 241 | return ce('tr', { key }, 242 | this.getNameColumn(), 243 | ce('td', null, 244 | this.innerReact(props))); 245 | } 246 | 247 | /* Each subclass will implement these abstract methods differently. 248 | - getValue should parse the value in the string and return the (typed) value for that 249 | parameter. For example, integer parameters will use parseInt here. 250 | - defaultValueRequired should return the initial value if the endpoint is required (e.g. 251 | 0 for integers, '' for strings). 252 | - innerReact determines how to render the input field for a parameter. 253 | */ 254 | getValue = (s: string): any => s; 255 | 256 | defaultValueRequired = (): any => ''; 257 | 258 | innerReact = (props: Dict): any => null; 259 | } 260 | 261 | export const parameterInput = (props: Dict) => { 262 | props.className = 'parameter-input'; 263 | return react.createElement( 264 | 'input', 265 | props, 266 | ); 267 | }; 268 | 269 | // A parameter whose value is a string. 270 | export class TextParam extends Parameter { 271 | innerReact = (props: Dict): react.DetailedReactHTMLElement => parameterInput(props); 272 | } 273 | 274 | // A parameter whose value is an integer. 275 | export class IntParam extends Parameter { 276 | innerReact = (props: Dict): react.DetailedReactHTMLElement => parameterInput(props); 277 | 278 | getValue = (s: string): number => ((s === '') ? this.defaultValue() : parseInt(s, 10)); 279 | 280 | defaultValueRequired = (): number => 0; 281 | } 282 | 283 | /* A parameter whose value is a float. 284 | This isn't currently used in our API, but could be in the future. 285 | */ 286 | export class FloatParam extends Parameter { 287 | innerReact = (props: Dict): react.DetailedReactHTMLElement => parameterInput(props); 288 | 289 | getValue = (s: string): number => ((s === '') ? this.defaultValue() : parseFloat(s)); 290 | 291 | defaultValueRequired = (): number => 0; 292 | } 293 | 294 | /* A parameter whose type is void. 295 | */ 296 | export class VoidParam extends Parameter { 297 | constructor(name: string) { 298 | super(name, true); 299 | } 300 | 301 | defaultValueRequired = (): number => 0; 302 | 303 | getValue = (s: string): string => null; 304 | } 305 | 306 | export class SelectorParam extends Parameter { 307 | choices: string[]; 308 | 309 | selected: string; 310 | 311 | constructor(name: string, optional: boolean, choices: string[], selected: string = null) { 312 | super(name, optional); 313 | 314 | this.choices = choices; 315 | if (this.optional) { 316 | this.choices.unshift(''); 317 | } 318 | 319 | this.selected = selected; 320 | } 321 | 322 | defaultValueRequired = (): any => this.choices[0]; 323 | 324 | getValue = (s: string): any => s; 325 | 326 | innerReact = (props: Dict): react.DetailedReactHTMLElement => { 327 | if (this.selected != null) { 328 | props.value = this.selected; 329 | } 330 | 331 | return ce('select', props, 332 | this.choices.map((choice:string) => ce('option', { 333 | key: choice, 334 | value: choice, 335 | }, choice))); 336 | } 337 | } 338 | 339 | // Booleans are selectors for true or false. 340 | export class BoolParam extends SelectorParam { 341 | constructor(name: string, optional: boolean) { 342 | super(name, optional, ['false', 'true']); 343 | } 344 | 345 | defaultValueRequired = (): any => false; 346 | 347 | getValue = (s: string): boolean => s === 'true'; 348 | } 349 | 350 | /* Upload-style endpoints accept data to upload. This is implemented as a special parameter 351 | to each endpoint, with the special name __file__. However, it's not technically an 352 | argument to its endpoint: the file is handled separately from the other parameters, since 353 | its contents are treated as data. 354 | Note that, since the name is fixed, only one file parameter can be used per endpoint right 355 | now. 356 | */ 357 | export class FileParam extends Parameter { 358 | constructor() { 359 | super('__file__', false); 360 | } 361 | 362 | innerReact = (props: Dict): react.DetailedReactHTMLElement => { 363 | props.type = 'file'; 364 | return parameterInput(props); 365 | } 366 | } 367 | 368 | /* A few parameters are structs whose fields are other parameters. The user will just see the 369 | fields as if they were top-level parameters, but the backend collects them into one 370 | dictionary. 371 | TODO: can structs be optional? If so, how do I hint this to the user? 372 | */ 373 | export class StructParam extends Parameter { 374 | fields: Parameter[]; 375 | 376 | constructor(name: string, optional: boolean, fields: Parameter[]) { 377 | super(name, optional); 378 | this.fields = fields; 379 | } 380 | 381 | populateFields = (dict: Dict): void => { 382 | this.fields.forEach((field: Parameter) => { 383 | if (!field.optional) { 384 | dict[field.name] = field.defaultValue(); 385 | } 386 | }); 387 | }; 388 | 389 | defaultValueRequired = (): Dict => { 390 | const toReturn: Dict = {}; 391 | this.populateFields(toReturn); 392 | return toReturn; 393 | } 394 | } 395 | 396 | // Union are selectors with multiple fields. 397 | export class UnionParam extends StructParam { 398 | getSelectorParam = (selected: string = null): SelectorParam => { 399 | const choices: string[] = []; 400 | this.fields.forEach((p) => choices.push(p.name)); 401 | 402 | return new SelectorParam(this.getSelectorName(), this.optional, choices, selected); 403 | }; 404 | 405 | getSelectorName = (): string => this.name; 406 | 407 | defaultValueRequired = (): Dict => { 408 | const param: Parameter = this.fields[0]; 409 | const toReturn: Dict = { '.tag': param.name }; 410 | 411 | if (param instanceof StructParam) { 412 | (param).populateFields(toReturn); 413 | } else if (!(param instanceof VoidParam)) { 414 | toReturn[param.name] = param.defaultValue(); 415 | } 416 | 417 | return toReturn; 418 | } 419 | } 420 | 421 | export class RootUnionParam extends UnionParam { 422 | getSelectorName = (): string => 'tag'; 423 | } 424 | 425 | export class ListParam extends Parameter { 426 | creator: (index: string) => Parameter; 427 | 428 | constructor(name: string, optional: boolean, creator: (index: string) => Parameter) { 429 | super(name, optional); 430 | this.creator = creator; 431 | } 432 | 433 | createItem = (index: number): Parameter => this.creator(index.toString()); 434 | 435 | defaultValue = (): List => [] 436 | } 437 | 438 | // Utilities for token flow 439 | const csrfTokenStorageName = 'Dropbox_API_state'; 440 | const tokenStorageName = 'Dropbox_API_explorer_token'; 441 | const clientIdStorageName = 'Dropbox_API_explorer_client_id'; 442 | 443 | export const getAuthType = (): AuthType => (window.location.href.indexOf('/team') > 0 444 | ? AuthType.Team 445 | : AuthType.User); 446 | 447 | export const createCsrfToken = (): string => { 448 | const randomBytes = new Uint8Array(18); // multiple of 3 avoids base-64 padding 449 | 450 | // If available, use the cryptographically secure generator, otherwise use Math.random. 451 | const crypto: RandomSource = window.crypto || (window).msCrypto; 452 | if (crypto && crypto.getRandomValues && false) { 453 | crypto.getRandomValues(randomBytes); 454 | } else { 455 | for (let i = 0; i < randomBytes.length; i++) { 456 | randomBytes[i] = Math.floor(Math.random() * 256); 457 | } 458 | } 459 | 460 | const token = btoa(String.fromCharCode.apply(null, randomBytes)); // base64-encode 461 | LocalStorage.setItem(csrfTokenStorageName, token); 462 | return token; 463 | }; 464 | 465 | export const checkCsrfToken = (givenCsrfToken: string): boolean => { 466 | const expectedCsrfToken = LocalStorage.getItem(csrfTokenStorageName); 467 | if (expectedCsrfToken === null) return false; 468 | return givenCsrfToken === expectedCsrfToken; // TODO: timing attack in string comparison? 469 | }; 470 | 471 | // A utility to read the URL's hash and parse it into a dict. 472 | export const getHashDict = (): Dict => { 473 | const toReturn: Dict = {}; 474 | const index: number = window.location.href.indexOf('#'); 475 | 476 | if (index === -1) return toReturn; 477 | 478 | const hash = window.location.href.substr(index + 1); 479 | const hashes: string[] = hash.split('#'); 480 | hashes.forEach((s: string) => { 481 | if (s.indexOf('&') === -1) toReturn.__ept__ = decodeURIComponent(s); 482 | else { 483 | s.split('&').forEach((pair: string) => { 484 | const splitPair = pair.split('='); 485 | toReturn[decodeURIComponent(splitPair[0])] = decodeURIComponent(splitPair[1]); 486 | }); 487 | } 488 | }); 489 | return toReturn; 490 | }; 491 | 492 | // Reading and writing the token, which is preserved in LocalStorage. 493 | export const putToken = (token: string): void => { 494 | LocalStorage.setItem(`${tokenStorageName}_${getAuthType()}`, token); 495 | }; 496 | 497 | export const getToken = (): string => LocalStorage.getItem(`${tokenStorageName}_${getAuthType()}`); 498 | 499 | // Reading and writing the client id, which is preserved in LocalStorage. 500 | export const putClientId = (clientId: string): void => { 501 | LocalStorage.setItem(`${clientIdStorageName}_${getAuthType()}`, clientId); 502 | }; 503 | 504 | export const getClientId = (): string => LocalStorage.getItem(`${clientIdStorageName}_${getAuthType()}`); 505 | 506 | // Some utilities that help with processing user input 507 | 508 | // Returns an endpoint given its name, or null if there was none 509 | export const getEndpoint = (epts: Endpoint[], name: string): Endpoint => { 510 | for (let i = 0; i < epts.length; i++) { 511 | if (epts[i].getFullName() === name) return epts[i]; 512 | } 513 | return null; // signals an error 514 | }; 515 | 516 | /* Returns the intial values for the parameters of an endpoint. Specifically, the non-optional 517 | parameters' initial values are put into the paramVals dictionary. This ensures that the 518 | required parameters are never missing when the 'submit' button is pressed. 519 | If there are no parameters (except possibly a file), then the dict should be null rather 520 | than an empty dict. 521 | */ 522 | export const initialValues = (ept: Endpoint): Dict => { 523 | if (ept.params.length === 0) return null; 524 | if (ept.params.length === 1 && ept.params[0].name === '__file__') return null; 525 | 526 | let toReturn: Dict = {}; 527 | ept.params.forEach((param: Parameter) => { 528 | if (!param.optional && param.name !== '__file__') { 529 | if (param instanceof RootUnionParam) { 530 | toReturn = param.defaultValue(); 531 | } else { 532 | toReturn[param.name] = param.defaultValue(); 533 | } 534 | } 535 | }); 536 | 537 | return toReturn; 538 | }; 539 | 540 | /* For a download endpoint, this function calculates the filename that the data should be saved 541 | as. First, it takes the basename of the 'path' argument, and then changes the extension for 542 | the get_thumbnail endpoint (which is a special case). 543 | This function assumes every download-style endpoint has a parameter named 'path.' 544 | */ 545 | export const getDownloadName = (ept: Endpoint, paramVals: Dict): string => { 546 | if (paramVals !== null && 'path' in paramVals) { 547 | let toReturn = paramVals.path.split('/').pop(); 548 | if (ept.name === 'get_thumbnail') { 549 | const format = ('format' in paramVals) ? paramVals.format['.tag'] : 'jpeg'; 550 | toReturn = `${toReturn.substr(0, toReturn.lastIndexOf('.'))}.${format}`; 551 | } 552 | return toReturn; 553 | } return ''; // not a download-style endpoint anyways 554 | }; 555 | 556 | // Returns the current URL without any fragment 557 | export const currentURL = (): string => window.location.href.split('#', 1)[0]; 558 | 559 | export const strippedCurrentURL = (): string => { 560 | const currentUrl = currentURL(); 561 | if (currentUrl.includes('?')) { 562 | return currentUrl.substring(0, currentUrl.indexOf('?')); 563 | } 564 | return currentUrl; 565 | }; 566 | 567 | export const arrayBufToString = (buf: ArrayBuffer): string => String.fromCharCode 568 | .apply(null, new Uint8Array(buf)); 569 | 570 | const isJson = (s: string): boolean => { 571 | try { 572 | JSON.parse(s); 573 | return true; 574 | } catch (_) { 575 | return false; 576 | } 577 | }; 578 | 579 | // Applies pretty-printing to JSON data serialized as a string. 580 | export const prettyJson = (s: string): string => JSON.stringify(JSON.parse(s), null, 2); 581 | 582 | // common message for error handling 583 | export const errorHandler = (stat: number, response: string): 584 | react.DetailedReactHTMLElement => { 585 | if (isJson(response)) return ce('code', { className: null, children: null }, prettyJson(response)); 586 | return react.createElement('span', null, [ 587 | react.createElement('h4', null, `Error: ${stat}`), 588 | react.createElement('code', null, response), 589 | ]); 590 | }; 591 | 592 | // Since HTTP headers cannot contain arbitrary Unicode characters, we must replace them. 593 | export const escapeUnicode = (s: string): string => s.replace(/[\u007f-\uffff]/g, 594 | (c: string) => `\\u${(`0000${c.charCodeAt(0).toString(16)}`).slice(-4)}`); 595 | 596 | // Used to get highlight.js to syntax-highlight the codeview and response areas. 597 | // Source: https://github.com/akiran/react-highlight/blob/main/src/index.jsx 598 | interface HltProps { 599 | className: string; 600 | children: react.ClassicElement> 601 | } 602 | export class Highlight extends react.Component> { 603 | defaultProps = { className: '' }; 604 | 605 | // TODO: fix this highlighting it breaks updates 606 | // componentDidMount = () => this.highlightCode(); 607 | // componentDidUpdate = () => this.highlightCode(); 608 | 609 | // highlightCode = () => [].forEach.call( 610 | // (reactDom.findDOMNode(this)).querySelectorAll('pre code'), 611 | // (node: Node) => hljs.highlightBlock(node) 612 | // ); 613 | 614 | public render(): ReactNode { 615 | return react.createElement('pre', { className: this.props.className }, 616 | react.createElement('code', { className: this.props.className }, 617 | this.props.children)); 618 | } 619 | } 620 | 621 | // Utility functions for getting the headers for an API call 622 | 623 | // The headers for an RPC-like endpoint HTTP request 624 | export const RPCLikeHeaders = (token: string, authType: AuthType): Dict => { 625 | const toReturn: Dict = {}; 626 | if (authType === AuthType.None) { 627 | // No auth headered for no auth endpoints. 628 | } else if (authType === AuthType.App) { 629 | toReturn.Authorization = 'Basic :'; 630 | } else { 631 | toReturn.Authorization = `Bearer ${token}`; 632 | } 633 | toReturn['Content-Type'] = 'application/json'; 634 | return toReturn; 635 | }; 636 | 637 | // args may need to be modified by the client, so they're passed in as a string 638 | export const uploadLikeHeaders = (token: string, args: string): Dict => ({ 639 | Authorization: `Bearer ${token}`, 640 | 'Content-Type': 'application/octet-stream', 641 | 'Dropbox-API-Arg': escapeUnicode(args), 642 | }); 643 | export const downloadLikeHeaders = (token: string, args: string): Dict => ({ 644 | Authorization: `Bearer ${token}`, 645 | 'Dropbox-API-Arg': escapeUnicode(args), 646 | }); 647 | 648 | export const getHeaders = (ept: Endpoint, token: string, customHeaders: Header[], 649 | args: string = null): Dict => { 650 | let headers: Dict = {}; 651 | 652 | switch (ept.getEndpointKind()) { 653 | case EndpointKind.RPCLike: { 654 | headers = RPCLikeHeaders(token, ept.getAuthType()); 655 | break; 656 | } 657 | case EndpointKind.Upload: { 658 | headers = uploadLikeHeaders(token, args); 659 | break; 660 | } 661 | case EndpointKind.Download: { 662 | headers = downloadLikeHeaders(token, args); 663 | break; 664 | } 665 | default: { 666 | throw new Error('Unknown endpoint type'); 667 | } 668 | } 669 | 670 | customHeaders.forEach((header) => { 671 | if (header.name !== '') { 672 | headers[header.name] = header.value; 673 | } 674 | }); 675 | 676 | return headers; 677 | }; 678 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /* The main file, which contains the definitions of the React components for the API Explorer, as 2 | well as a little bit of code that runs at startup. 3 | 4 | Each component is defined as an ES6 class extending the ReactComponent class. First, we declare 5 | the property types of the class, and then we declare the class itself. 6 | */ 7 | 8 | import * as react from 'react'; 9 | import * as reactDom from 'react-dom'; 10 | import * as endpoints from './endpoints'; 11 | import * as utils from './utils'; 12 | import * as apicalls from './apicalls'; 13 | import * as codeview from './codeview'; 14 | import { 15 | SelectorParam, 16 | Parameter, 17 | VoidParam, 18 | StructParam, 19 | Dict, 20 | UnionParam, 21 | FileParam, 22 | ListParam, 23 | List, 24 | RootUnionParam, 25 | Header, 26 | Endpoint, 27 | } from './utils'; 28 | 29 | import ReactElement = react.ReactElement; 30 | import HTMLAttributes = react.HTMLAttributes; 31 | 32 | // A few definitions to make code less verbose 33 | 34 | interface FileElement extends HTMLElement { 35 | files: File[] 36 | } 37 | 38 | const ce = react.createElement; 39 | 40 | const developerPage = 'https://www.dropbox.com/developers'; 41 | const displayNone = { style: { display: 'none' } }; 42 | 43 | /* Element for text field in page table. 44 | */ 45 | const tableText = (text: string): react.DetailedReactHTMLElement => ce('td', { className: 'label' }, 46 | ce('div', { className: 'text' }, text)); 47 | 48 | /* Map between client id and associated permission type. 49 | */ 50 | const clientIdMap: Dict = { 51 | vyjzkx2chlpsooc: 'Team Information', 52 | pq2bj4ll002gohi: 'Team Auditing', 53 | j3zzv20pgxds87u: 'Team Member File Access', 54 | oq1ywlcgrto51qk: 'Team Member Management', 55 | }; 56 | 57 | /* Get client id from local storage. If doesn't exist. Use default value instead. 58 | */ 59 | const getClientId = (): string => { 60 | const clientId = utils.getClientId(); 61 | 62 | if (clientId != null) { 63 | return clientId; 64 | } 65 | 66 | return utils.getAuthType() == utils.AuthType.User 67 | ? 'cg750anjts67v15' 68 | : 'vyjzkx2chlpsooc'; 69 | }; 70 | 71 | /* The dropdown menu to select app permission type for business endpoints. For each 72 | business endpoint. Only certain permission type would work and this component maps each 73 | permission type to associated client id. 74 | */ 75 | class AppPermissionInputProps { 76 | handler: (e: react.FormEvent) => void; 77 | } 78 | class AppPermissionInput extends react.Component { 79 | constructor(props: AppPermissionInputProps) { super(props); } 80 | 81 | public render() { 82 | const options: react.DetailedReactHTMLElement[] = []; 83 | const clientId = getClientId(); 84 | 85 | for (const id in clientIdMap) { 86 | const value : string = clientIdMap[id]; 87 | const selected : boolean = id == clientId; 88 | options.push(ce('option', { selected, className: null, children: null }, value)); 89 | } 90 | 91 | return ce('tr', null, 92 | tableText('App Permission'), 93 | ce('td', null, 94 | ce('select', { style: { 'margin-top': '5px' }, onChange: this.props.handler }, options))); 95 | } 96 | } 97 | 98 | /* The TokenInput area governs the authentication token used to issue requests. The user can enter 99 | a token or click to get a one, and can click another button to toggle showing/hiding the token. 100 | */ 101 | interface TokenInputProps { 102 | showToken: boolean; 103 | toggleShow: () => void, 104 | callback: (value: string) => void 105 | } 106 | class TokenInput extends react.Component { 107 | constructor(props: TokenInputProps) { super(props); } 108 | 109 | handleEdit = (event: react.FormEvent): void => { 110 | const { value } = event.target; 111 | this.props.callback(value); 112 | }; 113 | 114 | // This function handles the initial part of the OAuth2 token flow for the user. 115 | retrieveAuth = () => { 116 | const clientId = getClientId(); 117 | 118 | const state = `${utils.getHashDict().__ept__}!${utils.createCsrfToken()}`; 119 | const params: Dict = { 120 | response_type: 'token', 121 | client_id: clientId, 122 | redirect_uri: utils.strippedCurrentURL(), 123 | state, 124 | token_access_type: 'online', 125 | }; 126 | 127 | let urlWithParams = 'https://www.dropbox.com/oauth2/authorize?'; 128 | for (const key in params) { 129 | urlWithParams += `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}&`; 130 | } 131 | window.location.assign(urlWithParams); 132 | }; 133 | 134 | public render() { 135 | return ce('tr', null, 136 | tableText('Access Token'), 137 | ce('td', null, 138 | ce('input', { 139 | type: this.props.showToken ? 'text' : 'password', 140 | id: 'token-input', 141 | defaultValue: utils.getToken(), 142 | onChange: this.handleEdit, 143 | placeholder: 'If you don\'t have an access token, click the "Get Token" button to obtain one.', 144 | }), 145 | ce('div', { className: 'align-right' }, 146 | ce('button', { onClick: this.retrieveAuth }, 'Get Token'), 147 | ce('button', { onClick: this.props.toggleShow }, 148 | this.props.showToken ? 'Hide Token' : 'Show Token')))); 149 | } 150 | } 151 | 152 | /* Input component for single parameter. 153 | A value handler is responsible for value update and signal for specific parameter. 154 | Every time a field value gets updated, the update method of its corresponding value 155 | handler should be called. 156 | */ 157 | 158 | class ValueHandler { 159 | // Signal react render. 160 | update = (): void => null; 161 | 162 | // Update value for current parameter. 163 | updateValue = (value: any): void => null; 164 | } 165 | 166 | /* Type of value handler which can contain child value handlers. 167 | */ 168 | class ParentValueHandler extends ValueHandler { 169 | // Create a child value handler based on parameter type. 170 | getChildHandler = (param: Parameter): ValueHandler => { 171 | if (param instanceof FileParam) { 172 | return new FileValueHandler(param, this); 173 | } 174 | if (param instanceof RootUnionParam) { 175 | return new RootUnionValueHandler(param, this); 176 | } 177 | if (param instanceof UnionParam) { 178 | return new UnionValueHandler(param, this); 179 | } 180 | if (param instanceof StructParam) { 181 | return new StructValueHandler(param, this); 182 | } 183 | if (param instanceof ListParam) { 184 | return new ListValueHandler(param, this); 185 | } 186 | 187 | return new ChildValueHandler(param, this); 188 | }; 189 | 190 | getOrCreate = (name: string, defaultValue: any): any => { 191 | const dict: Dict = this.current(); 192 | if (name in dict) { 193 | return dict[name]; 194 | } 195 | 196 | dict[name] = defaultValue; 197 | return dict[name]; 198 | }; 199 | 200 | hasChild = (name: string): boolean => { 201 | const dict:Dict = this.current(); 202 | 203 | if (name in dict) { 204 | return true; 205 | } 206 | 207 | return false; 208 | }; 209 | 210 | value = (key: string): any => { 211 | const dict: Dict = this.current(); 212 | if (key in dict) { 213 | return dict[key]; 214 | } 215 | 216 | return null; 217 | }; 218 | 219 | updateChildValue = (name: string, value: any): void => { 220 | const dict:Dict = this.current(); 221 | 222 | if (value == null) { 223 | delete dict[name]; 224 | } else { 225 | dict[name] = value; 226 | } 227 | }; 228 | 229 | current = (): Dict|List => { throw new Error('Not implemented.'); }; 230 | } 231 | 232 | /* Value handler for struct type. 233 | */ 234 | class StructValueHandler extends ParentValueHandler { 235 | param: StructParam; 236 | 237 | parent: ParentValueHandler; 238 | 239 | constructor(param: StructParam, parent: ParentValueHandler) { 240 | super(); 241 | this.param = param; 242 | this.parent = parent; 243 | } 244 | 245 | add = (): void => { 246 | if (!this.param.optional) { 247 | throw new Error('Add is only support for optional parameter.'); 248 | } 249 | 250 | this.current(); 251 | this.update(); 252 | }; 253 | 254 | reset = (): void => { 255 | if (!this.param.optional) { 256 | throw new Error('Reset is only support for optional parameter.'); 257 | } 258 | 259 | this.parent.updateChildValue(this.param.name, this.param.defaultValue()); 260 | this.update(); 261 | }; 262 | 263 | current = (): Dict => this.parent.getOrCreate(this.param.name, {}); 264 | 265 | update = (): void => this.parent.update(); 266 | } 267 | 268 | /* Value handler for union type. 269 | */ 270 | class UnionValueHandler extends StructValueHandler { 271 | constructor(param: UnionParam, parent: ParentValueHandler) { 272 | super(param, parent); 273 | } 274 | 275 | getTag = (): string => { 276 | if (this.parent.hasChild(this.param.name)) { 277 | return this.value('.tag'); 278 | } 279 | 280 | return null; 281 | }; 282 | 283 | updateTag = (tag: string): void => { 284 | this.parent.updateChildValue(this.param.name, this.param.optional ? null : {}); 285 | 286 | if (tag != null) { 287 | this.updateChildValue('.tag', tag); 288 | } 289 | }; 290 | 291 | getTagHandler = (): TagValueHandler => new TagValueHandler(this) 292 | } 293 | 294 | /* Special case when root type is a union. 295 | */ 296 | class RootUnionValueHandler extends UnionValueHandler { 297 | constructor(param: RootUnionParam, handler: RootValueHandler) { 298 | super(param, handler); 299 | } 300 | 301 | getTag = (): string => this.value('.tag'); 302 | 303 | updateTag = (tag: string): void => { 304 | const dict: Dict = this.current(); 305 | 306 | for (const name in dict) { 307 | delete dict[name]; 308 | } 309 | 310 | if (tag != null) { 311 | dict['.tag'] = tag; 312 | } 313 | }; 314 | 315 | current = (): Dict => this.parent.current(); 316 | 317 | update = (): void => this.parent.update(); 318 | 319 | getTagHandler = (): TagValueHandler => new TagValueHandler(this) 320 | } 321 | 322 | /* Value handler for list type. 323 | */ 324 | class ListValueHandler extends ParentValueHandler { 325 | param: ListParam; 326 | 327 | parent: ParentValueHandler; 328 | 329 | constructor(param: ListParam, parent: ParentValueHandler) { 330 | super(); 331 | this.param = param; 332 | this.parent = parent; 333 | } 334 | 335 | addItem = (): void => { 336 | const list: List = this.current(); 337 | const param: Parameter = this.param.createItem(0); 338 | list.push(param.defaultValue()); 339 | this.update(); 340 | }; 341 | 342 | reset = (): void => { 343 | this.parent.updateChildValue(this.param.name, this.param.defaultValue()); 344 | this.update(); 345 | }; 346 | 347 | getOrCreate = (name: string, defaultValue: any): any => this.current()[+name]; 348 | 349 | hasChild = (name: string) => true; 350 | 351 | value = 352 | (key: string): any => this.current()[+name]; // eslint-disable-line no-restricted-globals 353 | 354 | updateChildValue = (name: string, value: any): void => { 355 | this.current()[+name] = value; 356 | }; 357 | 358 | current = (): List => this.parent.getOrCreate(this.param.name, []); 359 | 360 | update = (): void => this.parent.update(); 361 | } 362 | 363 | /* Value handler for primitive types. 364 | */ 365 | class ChildValueHandler extends ValueHandler { 366 | param: T; 367 | 368 | parent: S; 369 | 370 | constructor(param: T, parent: S) { 371 | super(); 372 | this.param = param; 373 | this.parent = parent; 374 | } 375 | 376 | updateValue = (value: any): void => { 377 | this.parent.updateChildValue(this.param.name, value); 378 | }; 379 | 380 | update = (): void => this.parent.update(); 381 | } 382 | 383 | /* Value handler for file parameter. 384 | */ 385 | class FileValueHandler extends ChildValueHandler { 386 | constructor(param: FileParam, parent: RootValueHandler) { 387 | super(param, parent); 388 | } 389 | 390 | // Update value of current parameter. 391 | updateValue = (value: any): void => { 392 | this.parent.updateFile(value); 393 | } 394 | } 395 | 396 | /* Value handler for union tag. 397 | */ 398 | class TagValueHandler extends ChildValueHandler { 399 | constructor(parent: UnionValueHandler) { 400 | super(null, parent); 401 | } 402 | 403 | updateValue = (value: any): void => { 404 | this.parent.updateTag(value); 405 | } 406 | } 407 | 408 | /* Value handler for root. 409 | */ 410 | class RootValueHandler extends ParentValueHandler { 411 | paramVals: Dict; 412 | 413 | fileVals: Dict; 414 | 415 | callback: (params: Dict, file: Dict) => void; 416 | 417 | constructor(paramVals: Dict, fileVals: Dict, callback: (params: Dict, files: Dict) => void) { 418 | super(); 419 | this.paramVals = paramVals; 420 | this.fileVals = fileVals; 421 | this.callback = callback; 422 | } 423 | 424 | current = ():Dict => this.paramVals; 425 | 426 | update = (): void => this.callback(this.paramVals, this.fileVals); 427 | 428 | updateFile = (value: string) => this.fileVals.file = value; 429 | } 430 | 431 | class ParamInput

extends react.Component { 432 | constructor(props: P) { 433 | super(props); 434 | } 435 | 436 | public render(): any { // eslint-disable-line class-methods-use-this 437 | throw new Error('Not implemented.'); 438 | } 439 | } 440 | 441 | // The ParamInput area handles the input field of a single parameter to an endpoint. 442 | interface SingleParamInputProps { 443 | key: string; 444 | handler: ValueHandler; 445 | param: Parameter 446 | } 447 | 448 | /* Input component for single parameter. 449 | */ 450 | class SingleParamInput extends ParamInput { 451 | constructor(props: SingleParamInputProps) { 452 | super(props); 453 | } 454 | 455 | // When the field is edited, its value is parsed and the state is updated. 456 | handleEdit = (event: Event) => { 457 | let valueToReturn: any = null; 458 | // special case: the target isn't an HTMLInputElement 459 | if (this.props.param.name === '__file__') { 460 | const fileTarget: FileElement = event.target; 461 | if (fileTarget.files.length > 0) valueToReturn = fileTarget.files[0]; 462 | } else { 463 | const target: HTMLInputElement = event.target; 464 | /* If valueToReturn is left as null, it signals an optional value that should be 465 | deleted from the dict of param values. 466 | */ 467 | if (target.value !== '' || !this.props.param.optional) { 468 | valueToReturn = this.props.param.getValue(target.value); 469 | } 470 | } 471 | this.props.handler.updateValue(valueToReturn); 472 | this.props.handler.update(); 473 | }; 474 | 475 | public render() { 476 | return this.props.param.asReact({ onChange: this.handleEdit }, this.props.key); 477 | } 478 | } 479 | 480 | /* Some parameters are structs of other parameters, e.g. in upload_session/finish. In the input 481 | field, structs are treated as just a list of parameters. This means we currently can't really 482 | signal optional structs to the user. Moreover, nested structs are currently not possible. 483 | */ 484 | interface StructParamInputProps { 485 | key: string; 486 | handler: StructValueHandler; 487 | param: utils.StructParam; 488 | } 489 | class StructParamInput extends ParamInput { 490 | constructor(props: StructParamInputProps) { 491 | super(props); 492 | this.state = { display: !props.param.optional }; 493 | } 494 | 495 | add = () => { 496 | this.props.handler.add(); 497 | this.setState({ display: true }); 498 | }; 499 | 500 | reset = () => { 501 | this.props.handler.reset(); 502 | this.setState({ display: false }); 503 | }; 504 | 505 | public render() { 506 | return ce('tr', null, 507 | this.props.param.getNameColumn(), 508 | ce('td', null, 509 | ce('table', null, 510 | ce('tbody', null, this.renderItems())))); 511 | } 512 | 513 | renderItems = (): react.DetailedReactHTMLElement[] => { 514 | const ret: react.DetailedReactHTMLElement[] = []; 515 | 516 | if (this.state.display || !this.props.param.optional) { 517 | for (const p of this.props.param.fields) { 518 | const input = ParamClassChooser.getParamInput(p, { 519 | key: `${this.props.key}_${this.props.param.name}_${p.name}`, 520 | handler: this.props.handler.getChildHandler(p), 521 | param: p, 522 | }); 523 | 524 | ret.push(input); 525 | } 526 | } 527 | 528 | if (this.props.param.optional) { 529 | const button = this.state.display 530 | ? ce('button', { onClick: this.reset }, 'Clear') 531 | : ce('button', { onClick: this.add }, 'Add'); 532 | 533 | ret.push(ce('tr', { className: 'struct-param-actions' }, 534 | ce('td', null, button))); 535 | } 536 | 537 | return ret; 538 | } 539 | } 540 | 541 | interface UnionParamInputProps { 542 | key: string; 543 | handler: UnionValueHandler; 544 | param: utils.UnionParam; 545 | } 546 | class UnionParamInput extends ParamInput { 547 | constructor(props: UnionParamInputProps) { 548 | super(props); 549 | } 550 | 551 | getParam = (): StructParam => { 552 | const tag: string = this.props.handler.getTag(); 553 | let fields: Parameter[] = null; 554 | if (tag == null) { 555 | fields = []; 556 | } else { 557 | const param: Parameter = this.props.param.fields.filter((t) => t.name == tag)[0]; 558 | 559 | if (param instanceof StructParam) { 560 | fields = (param).fields; 561 | } else if (param instanceof VoidParam) { 562 | fields = []; 563 | } else { 564 | fields = [param]; 565 | } 566 | } 567 | 568 | return new StructParam(this.props.param.name, false, fields); 569 | }; 570 | 571 | public render() { 572 | const selectParamProps: SingleParamInputProps = { 573 | key: `${this.props.key}_selector`, 574 | handler: this.props.handler.getTagHandler(), 575 | param: this.props.param.getSelectorParam(this.props.handler.getTag()), 576 | }; 577 | 578 | const param = this.getParam(); 579 | 580 | if (param.fields.length == 0) { 581 | return ce(SingleParamInput, selectParamProps); 582 | } 583 | 584 | const structParam = new StructParamInput({ 585 | key: `${this.props.key}_${param.name}`, 586 | handler: this.props.handler, 587 | param, 588 | }); 589 | 590 | return ce('tr', null, 591 | this.props.param.getNameColumn(), 592 | ce('td', null, 593 | ce('table', null, 594 | ce('tbody', null, [ce(SingleParamInput, selectParamProps)].concat(structParam.renderItems()))))); 595 | } 596 | } 597 | 598 | interface ListParamInputProps { 599 | key: string; 600 | handler: ListValueHandler; 601 | param: utils.ListParam; 602 | } 603 | class ListParamInput extends ParamInput { 604 | constructor(props: ListParamInputProps) { 605 | super(props); 606 | this.state = { count: 0 }; 607 | } 608 | 609 | addItem = (): void => { 610 | this.props.handler.addItem(); 611 | this.setState({ count: this.state.count + 1 }); 612 | }; 613 | 614 | reset = (): void => { 615 | this.props.handler.reset(); 616 | this.setState({ count: 0 }); 617 | }; 618 | 619 | public render() { 620 | return ce('tr', null, 621 | this.props.param.getNameColumn(), 622 | ce('td', null, 623 | ce('table', null, 624 | ce('tbody', null, this.renderItems())))); 625 | } 626 | 627 | renderItems = (): react.DetailedReactHTMLElement[] => { 628 | const ret: react.DetailedReactHTMLElement[] = []; 629 | for (let i = 0; i < this.state.count; i++) { 630 | const param: Parameter = this.props.param.createItem(i); 631 | const item: react.DetailedReactHTMLElement = ParamClassChooser 632 | .getParamInput(param, { 633 | key: `${this.props.key}_${this.props.param.name}_${i.toString()}`, 634 | handler: this.props.handler.getChildHandler(param), 635 | param, 636 | }); 637 | 638 | ret.push(item); 639 | } 640 | 641 | ret.push(ce('tr', { className: 'list-param-actions' }, 642 | ce('td', null, 643 | ce('button', { onClick: this.addItem }, 'Add'), 644 | ce('button', { onClick: this.reset }, 'Clear')))); 645 | 646 | return ret; 647 | } 648 | } 649 | 650 | // Picks the correct React class for a parameter, depending on whether it's a struct. 651 | class ParamClassChooser { 652 | public static getParamInput(param: Parameter, props: any): any { 653 | if (param instanceof utils.UnionParam) { 654 | return ce(UnionParamInput, props); 655 | } 656 | if (param instanceof utils.StructParam) { 657 | return ce(StructParamInput, props); 658 | } 659 | if (param instanceof utils.ListParam) { 660 | return ce(ListParamInput, props); 661 | } 662 | 663 | return ce(SingleParamInput, props); 664 | } 665 | } 666 | 667 | /* The code view section of the API Explorer. This component manages a selector which chooses what 668 | format to display the code in, as well as the div that contains the code view itself. 669 | */ 670 | interface CodeAreaProps { 671 | ept: Endpoint; 672 | paramVals: Dict; 673 | headerVals: Header[], 674 | __file__: File, 675 | token: string 676 | } 677 | class CodeArea extends react.Component { 678 | constructor(props: CodeAreaProps) { 679 | super(props); 680 | this.state = { formatter: codeview.formats.curl }; 681 | } 682 | 683 | changeFormat = (event: react.FormEvent) => { 684 | const newFormat = (event.target).value; 685 | this.setState({ formatter: codeview.formats[newFormat] }); 686 | }; 687 | 688 | public render() { 689 | return ce('span', { id: 'code-area' }, 690 | ce('p', null, 'View request as ', codeview.getSelector(this.changeFormat)), 691 | ce('span', null, codeview.render(this.state.formatter, this.props.ept, this.props.token, 692 | this.props.paramVals, this.props.headerVals, this.props.__file__))); 693 | } 694 | } 695 | 696 | /* A component handling all the main user-input areas of the document: the token area, the 697 | parameter areas, and the code view. Because of this, it holds a lot of state: the token and 698 | whether to show or hide it; the paramVals and file that are passed to the code viewer or 699 | submitted; and any error messages. 700 | */ 701 | interface RequestAreaProps { 702 | currEpt: Endpoint; 703 | APICaller: (paramsData: string, ept: Endpoint, token: string, 704 | headers: Header[], responseFn: apicalls.Callback, file: File) => void; 705 | inProgress: boolean 706 | } 707 | class RequestArea extends react.Component { 708 | constructor(props: RequestAreaProps) { 709 | super(props); 710 | this.state = { 711 | paramVals: utils.initialValues(this.props.currEpt), 712 | headerVals: [], 713 | fileVals: { file: null }, // a signal that no file has been chosen 714 | errMsg: null, 715 | showToken: true, 716 | showCode: false, 717 | showHeaders: false, 718 | }; 719 | } 720 | 721 | updateParamValues = (paramVals: Dict, fileVals: Dict) => { 722 | this.setState({ paramVals, fileVals }); 723 | this.forceUpdate(); 724 | }; 725 | 726 | updateHeaderValues = (headerVals: Header[]) => { 727 | this.setState({ headerVals }); 728 | this.forceUpdate(); 729 | }; 730 | 731 | updateTokenValue = (tokenValue: string) => { 732 | // This is called only to trigger live update. Use utils.getToken 733 | // to get latest token. 734 | utils.putToken(tokenValue); 735 | this.forceUpdate(); 736 | }; 737 | 738 | /* Called when a new endpoint is chosen or the user updates the token. If a new endpoint is 739 | chosen, we should initialize its parameter values; if a new token is chosen, any error 740 | message about the token no longer applies. 741 | */ 742 | componentWillReceiveProps = (newProps: RequestAreaProps) => { 743 | if (newProps.currEpt !== this.props.currEpt) { 744 | this.updateParamValues(utils.initialValues(newProps.currEpt), { file: null }); 745 | } 746 | 747 | this.setState({ errMsg: null }); 748 | }; 749 | 750 | /* Submits a call to the API. This function handles the display logic (e.g. whether or not to 751 | display an error message for a missing token), and the APICaller prop actually sends the 752 | request. 753 | */ 754 | submit = () => { 755 | const token = utils.getToken(); 756 | const { currEpt } = this.props; 757 | const authType = currEpt.getAuthType(); 758 | 759 | if (authType == utils.AuthType.App) { 760 | this.setState({ 761 | errMsg: 'Error: Making API call for app auth endpoint is not supported. Please run the code using credential of your own app.', 762 | }); 763 | } else if (authType != utils.AuthType.None && (token == null || token === '')) { 764 | this.setState({ 765 | errMsg: 'Error: missing token. Please enter a token above or click the "Get Token" button.', 766 | }); 767 | } else { 768 | this.setState({ errMsg: null }); 769 | const responseFn = apicalls.chooseCallback(currEpt.getEndpointKind(), 770 | utils.getDownloadName(currEpt, this.state.paramVals)); 771 | this.props.APICaller(JSON.stringify(this.state.paramVals), currEpt, 772 | token, this.state.headerVals, responseFn, this.state.fileVals.file); 773 | } 774 | }; 775 | 776 | // Toggles whether the token is hidden, or visible on the screen. 777 | showOrHide = () => this.setState({ showToken: !this.state.showToken }); 778 | 779 | // Toggles whether code block is visible. 780 | showOrHideCode = () => this.setState({ showCode: !this.state.showCode }); 781 | 782 | // Toggles whether header block is visible. 783 | showOrHideHeaders = () => this.setState({ showHeaders: !this.state.showHeaders }); 784 | 785 | // Update client id when app permission change. 786 | updateClientId = (e: react.FormEvent): void => { 787 | const { value } = (e.target); 788 | for (const id in clientIdMap) { 789 | if (clientIdMap[id] == value) { 790 | utils.putClientId(id); 791 | return; 792 | } 793 | } 794 | }; 795 | 796 | public render() { 797 | let errMsg: any = []; 798 | 799 | if (this.state.errMsg != null) { 800 | errMsg = [ce('span', { style: { color: 'red' } }, this.state.errMsg)]; 801 | } 802 | 803 | const name = this.props.currEpt.name.replace('/', '-'); 804 | const documentation = `${developerPage}/documentation/http/documentation#${this.props.currEpt.ns}-${name}`; 805 | const handler = new RootValueHandler(this.state.paramVals, 806 | this.state.fileVals, this.updateParamValues); 807 | const headerHandler = new RequestHeaderRootHandler(this.state.headerVals, 808 | this.updateHeaderValues); 809 | 810 | return ce('span', { id: 'request-area' }, 811 | ce('table', { className: 'page-table' }, 812 | ce('tbody', null, 813 | utils.getAuthType() == utils.AuthType.Team 814 | ? ce(AppPermissionInput, { handler: this.updateClientId }) 815 | : null, 816 | ce(TokenInput, { 817 | toggleShow: this.showOrHide, 818 | showToken: this.state.showToken, 819 | callback: this.updateTokenValue, 820 | }), 821 | ce('tr', null, 822 | tableText('Request'), 823 | ce('td', null, 824 | ce('div', { className: 'align-right' }, 825 | ce('a', { href: documentation }, 826 | 'Documentation')), 827 | ce('table', { id: 'parameter-list' }, 828 | ce('tbody', null, 829 | this.props.currEpt.params.map( 830 | (param: Parameter) => ParamClassChooser.getParamInput(param, { 831 | key: this.props.currEpt.name + param.name, 832 | handler: handler.getChildHandler(param), 833 | param, 834 | }), 835 | ))), 836 | ce('div', null, 837 | ce('button', { onClick: this.showOrHideHeaders }, this.state.showHeaders ? 'Hide Headers' : 'Show Headers'), 838 | ce('button', { onClick: this.showOrHideCode }, this.state.showCode ? 'Hide Code' : 'Show Code'), 839 | ce('button', { onClick: this.submit, disabled: this.props.inProgress }, 'Submit Call'), 840 | ce('img', { 841 | src: 'https://www.dropbox.com/static/images/icons/ajax-loading-small.gif', 842 | hidden: !this.props.inProgress, 843 | style: { position: 'relative', top: '2px', left: '10px' }, 844 | }), 845 | errMsg))), 846 | ce('tr', this.state.showHeaders ? null : displayNone, 847 | tableText('Headers'), 848 | ce('td', null, 849 | ce('div', { id: 'request-headers' }, 850 | ce(RequestHeaderArea, { handler: headerHandler })))), 851 | ce('tr', this.state.showCode ? null : displayNone, 852 | tableText('Code'), 853 | ce('td', null, 854 | ce('div', { id: 'request-container' }, 855 | ce(CodeArea, { 856 | ept: this.props.currEpt, 857 | paramVals: this.state.paramVals, 858 | headerVals: this.state.headerVals, 859 | __file__: this.state.fileVals.file, 860 | token: this.state.showToken ? utils.getToken() : '', 861 | }))))))); 862 | } 863 | } 864 | 865 | interface RequestHeaderAreaProps { 866 | handler: RequestHeaderRootHandler 867 | } 868 | 869 | class RequestHeaderArea extends react.Component { 870 | constructor(props: RequestHeaderAreaProps) { 871 | super(props); 872 | } 873 | 874 | public render() { 875 | const { handler } = this.props; 876 | 877 | return ce('span', { id: 'request-header-area' }, 878 | ce('div', null, ce('button', { onClick: handler.add }, 'Add Header')), 879 | ce('table', null, 880 | ce('tbody', null, 881 | handler.getHeaders().map( 882 | (header: Header) => ce(RequestHeaderInput, { 883 | header, 884 | handler: new RequestHeaderHandler(handler), 885 | }), 886 | )))); 887 | } 888 | } 889 | 890 | class RequestHeaderRootHandler { 891 | headers: Header[]; 892 | 893 | callBack: (headers: Header[]) => void; 894 | 895 | constructor(headers: Header[], callback: (headers: Header[]) => void) { 896 | this.headers = headers; 897 | this.callBack = callback; 898 | } 899 | 900 | remove = (header: Header): void => { 901 | const index = this.headers.indexOf(header); 902 | this.headers.splice(index, 1); 903 | this.callBack(this.headers); 904 | }; 905 | 906 | add = (): void => { 907 | this.headers.push(new Header()); 908 | this.callBack(this.headers); 909 | }; 910 | 911 | update = (): void => { 912 | this.callBack(this.headers); 913 | }; 914 | 915 | getHeaders = (): Header[] => this.headers 916 | } 917 | 918 | class RequestHeaderHandler { 919 | parentHandler: RequestHeaderRootHandler; 920 | 921 | constructor(parentHandler: RequestHeaderRootHandler) { 922 | this.parentHandler = parentHandler; 923 | } 924 | 925 | onChange = (header: Header, removed: boolean) => { 926 | if (removed) { 927 | this.parentHandler.remove(header); 928 | } else { 929 | this.parentHandler.update(); 930 | } 931 | }; 932 | } 933 | 934 | interface RequestHeaderInputProps { 935 | header: Header, 936 | handler: RequestHeaderHandler 937 | } 938 | 939 | class RequestHeaderInput extends react.Component { 940 | constructor(props: RequestHeaderInputProps) { 941 | super(props); 942 | } 943 | 944 | public render() { 945 | return this.props.header.asReact(this.props.handler.onChange); 946 | } 947 | } 948 | 949 | /* A small component governing an endpoint on the sidebar, to bold it when it's selected and 950 | handle the logic when it is clicked. 951 | */ 952 | interface EndpointChoiceProps { 953 | key: string; 954 | ept: Endpoint; 955 | handleClick: (ept: Endpoint) => void; 956 | isSelected: boolean 957 | } 958 | class EndpointChoice extends react.Component { 959 | constructor(props: EndpointChoiceProps) { super(props); } 960 | 961 | onClick = () => this.props.handleClick(this.props.ept); 962 | 963 | public render() { 964 | return (this.props.isSelected) 965 | ? ce('li', null, ce('b', null, this.props.ept.name), ce('br', null)) 966 | : ce('li', null, ce('a', { onClick: this.onClick }, this.props.ept.name), ce('br', null)); 967 | } 968 | } 969 | 970 | /* The EndpointSelector component governs the list of endpoints on the sidebar, and propagates the 971 | information of which one is currently selected. 972 | */ 973 | interface EndpointSelectorProps { 974 | eptChanged: (ept: Endpoint) => void; 975 | currEpt: Endpoint 976 | } 977 | class EndpointSelector extends react.Component { 978 | constructor(props: EndpointSelectorProps) { super(props); } 979 | 980 | filter = (ept: Endpoint): boolean => { 981 | if (ept.params.length > 0 && ept.params.indexOf(null) >= 0) { 982 | // Skip not implemented endpoints. 983 | return true; 984 | } 985 | 986 | const eptAuthType = ept.getAuthType() == utils.AuthType.Team 987 | ? utils.AuthType.Team 988 | : utils.AuthType.User; 989 | 990 | if (eptAuthType != utils.getAuthType()) { 991 | // Skip endpoints with different auth type. 992 | return true; 993 | } 994 | 995 | return false; 996 | }; 997 | 998 | // Renders the logo and the list of endpoints 999 | public render() { 1000 | const groups: {[ns: string]: Endpoint[]} = {}; 1001 | const namespaces: string[] = []; 1002 | 1003 | endpoints.endpointList.forEach((ept: Endpoint) => { 1004 | if (this.filter(ept)) { 1005 | return; 1006 | } 1007 | 1008 | if (groups[ept.ns] == undefined) { 1009 | groups[ept.ns] = [ept]; 1010 | namespaces.push(ept.ns); 1011 | } else { 1012 | groups[ept.ns].push(ept); 1013 | } 1014 | }); 1015 | 1016 | return ce('div', { id: 'sidebar' }, 1017 | ce('p', { style: { marginLeft: '35px', marginTop: '12px' } }, 1018 | ce('a', { onClick: () => window.location.href = developerPage }, 1019 | ce('img', { 1020 | src: 'https://cfl.dropboxstatic.com/static/images/logo_catalog/blue_dropbox_glyph_m1-vflZvZxbS.png', 1021 | width: 36, 1022 | className: 'home-icon', 1023 | }))), 1024 | ce('div', { id: 'endpoint-list' }, 1025 | namespaces.sort().map((ns: string) => ce('div', null, 1026 | ce('li', null, ns), 1027 | groups[ns].map((ept: Endpoint) => ce(EndpointChoice, { 1028 | key: ept.name, 1029 | ept, 1030 | handleClick: this.props.eptChanged, 1031 | isSelected: this.props.currEpt == ept, 1032 | })))))); 1033 | } 1034 | } 1035 | 1036 | /* The React component for resposne area). 1037 | */ 1038 | interface ResponseAreaProps { 1039 | hide: boolean; 1040 | responseText: string; 1041 | downloadButton: any; 1042 | } 1043 | class ResponseArea extends react.Component { 1044 | constructor(props: ResponseAreaProps) { 1045 | super(props); 1046 | } 1047 | 1048 | public render() { 1049 | return ce('span', { id: 'response-area' }, 1050 | ce('table', { className: 'page-table' }, 1051 | ce('tbody', this.props.hide ? displayNone : null, 1052 | ce('tr', null, 1053 | tableText('Response'), 1054 | ce('td', null, 1055 | ce('div', { id: 'response-container' }, 1056 | ce(utils.Highlight, { className: 'json', children: null }, this.props.responseText)), 1057 | ce('div', null, this.props.downloadButton)))))); 1058 | } 1059 | } 1060 | 1061 | /* The top-level React component for the API Explorer (except text-based pages, such as the intro 1062 | page and the error pages). 1063 | */ 1064 | interface APIExplorerProps { 1065 | initEpt: Endpoint 1066 | } 1067 | class APIExplorer extends react.Component { 1068 | constructor(props: APIExplorerProps) { 1069 | super(props); 1070 | this.state = { 1071 | ept: this.props.initEpt, 1072 | downloadURL: '', 1073 | responseText: '', 1074 | inProgress: false, 1075 | }; 1076 | } 1077 | 1078 | componentWillReceiveProps = (newProps: APIExplorerProps) => this.setState({ 1079 | ept: newProps.initEpt, 1080 | downloadURL: '', 1081 | responseText: '', 1082 | }); 1083 | 1084 | APICaller = (paramsData: string, endpt: Endpoint, token: string, 1085 | headers: Header[], responseFn: apicalls.Callback, file: File) => { 1086 | this.setState({ inProgress: true }); 1087 | 1088 | const responseFn_wrapper: apicalls.Callback = (component: any, resp: XMLHttpRequest) => { 1089 | this.setState({ inProgress: false }); 1090 | responseFn(component, resp); 1091 | }; 1092 | 1093 | apicalls.APIWrapper(paramsData, endpt, token, headers, responseFn_wrapper, this, file); 1094 | }; 1095 | 1096 | public render() { 1097 | // This button pops up only on download 1098 | const downloadButton = (this.state.downloadURL !== '') 1099 | ? ce('a', { 1100 | href: this.state.downloadURL, 1101 | download: this.state.downloadFilename, 1102 | }, ce('button', null, `Download ${this.state.downloadFilename}`)) 1103 | : null; 1104 | 1105 | const props: MainPageProps = { 1106 | currEpt: this.state.ept, 1107 | header: ce('span', null, `Dropbox API Explorer • ${this.state.ept.name}`), 1108 | messages: [ 1109 | ce(RequestArea, { 1110 | currEpt: this.state.ept, 1111 | APICaller: this.APICaller, 1112 | inProgress: this.state.inProgress, 1113 | }), 1114 | ce(ResponseArea, { 1115 | hide: this.state.inProgress || this.state.responseText == '', 1116 | responseText: this.state.responseText, 1117 | downloadButton, 1118 | }), 1119 | ], 1120 | }; 1121 | 1122 | return ce(MainPage, props); 1123 | } 1124 | } 1125 | 1126 | /* This class renders the main page which contains endpoint selector 1127 | sidebar and main content page. 1128 | */ 1129 | interface MainPageProps { 1130 | currEpt: Endpoint; 1131 | header: react.ReactElement; 1132 | messages: react.ReactElement[]; 1133 | } 1134 | class MainPage extends react.Component { 1135 | constructor(props: MainPageProps) { super(props); } 1136 | 1137 | getAuthSwitch = (): react.DetailedReactHTMLElement => { 1138 | if (utils.getAuthType() == utils.AuthType.User) { 1139 | return ce('a', { id: 'auth-switch', href: `${utils.currentURL()}team/` }, 'Switch to Business endpoints'); 1140 | } 1141 | 1142 | return ce('a', { id: 'auth-switch', href: '../' }, 'Switch to User endpoints'); 1143 | }; 1144 | 1145 | public render() { 1146 | return ce('span', null, 1147 | ce(EndpointSelector, { 1148 | eptChanged: (endpt: Endpoint) => window.location.hash = `#${endpt.getFullName()}`, 1149 | currEpt: this.props.currEpt, 1150 | }), 1151 | ce('h1', { id: 'header' }, this.props.header, this.getAuthSwitch()), 1152 | ce('div', { id: 'page-content' }, 1153 | this.props.messages)); 1154 | } 1155 | } 1156 | 1157 | /* This class renders a text page (the intro page and error messages). Then, each page is an 1158 | instance of TextPage. 1159 | */ 1160 | interface TextPageProps { 1161 | message: react.DetailedReactHTMLElement; 1162 | } 1163 | class TextPage extends react.Component { 1164 | constructor(props: TextPageProps) { super(props); } 1165 | 1166 | public render() { 1167 | return ce(MainPage, { 1168 | currEpt: new Endpoint('', '', null), 1169 | header: ce('span', null, 'Dropbox API Explorer'), 1170 | messages: [this.props.message], 1171 | }); 1172 | } 1173 | } 1174 | 1175 | // Introductory page, which people see when they first open the webpage 1176 | const introPage: react.ReactElement = ce(TextPage, { 1177 | message: 1178 | ce('span', null, 1179 | ce('p', null, 'Welcome to the Dropbox API Explorer!'), 1180 | ce('p', null, 1181 | 'This API Explorer is a tool to help you learn about the ', 1182 | ce('a', { href: developerPage }, 'Dropbox API v2'), 1183 | ' and test your own examples. For each endpoint, you\'ll be able to submit an API call ', 1184 | 'with your own parameters and see the code for that call, as well as the API response.'), 1185 | ce('p', null, 1186 | 'Click on an endpoint on your left to get started, or check out ', 1187 | ce('a', { href: `${developerPage}/documentation` }, 1188 | 'the documentation'), 1189 | ' for more information on the API.')), 1190 | }); 1191 | 1192 | /* The endpoint name (supplied via the URL's hash) doesn't correspond to any actual endpoint. Right 1193 | now, this can only happen if the user edits the URL hash. 1194 | React sanitizes its inputs, so displaying the hash below is safe. 1195 | */ 1196 | const endpointNotFound: react.ReactElement = ce(TextPage, { 1197 | message: 1198 | ce('span', null, 1199 | ce('p', null, 'Welcome to the Dropbox API Explorer!'), 1200 | ce('p', null, 1201 | 'Unfortunately, there doesn\'t seem to be an endpoint called ', 1202 | ce('b', null, window.location.hash.substr(1)), 1203 | '. Try clicking on an endpoint on the left instead.'), 1204 | ce('p', null, 'If you think you received this message in error, please get in contact with us.')), 1205 | }); 1206 | 1207 | /* Error when the state parameter of the hash isn't what was expected, which could be due to an 1208 | XSRF attack. 1209 | */ 1210 | const stateError: react.ReactElement = ce(TextPage, { 1211 | message: 1212 | ce('span', null, 1213 | ce('p', null, ''), 1214 | ce('p', null, 1215 | 'Unfortunately, there was a problem retrieving your OAuth2 token; please try again. ', 1216 | 'If this error persists, you may be using an insecure network.'), 1217 | ce('p', null, 'If you think you received this message in error, please get in contact with us.')), 1218 | }); 1219 | 1220 | /* The hash of the URL determines which page to render; no hash renders the intro page, and 1221 | 'auth_error!' (the ! chosen so it's less likely to have a name clash) renders the stateError 1222 | page when the state parameter isn't what was expected. 1223 | */ 1224 | const renderGivenHash = (hash: string): void => { 1225 | if (hash === '' || hash === undefined) { 1226 | reactDom.render(introPage, document.body); 1227 | } else if (hash === 'xkcd') { 1228 | window.location.href = 'https://xkcd.com/1481/'; 1229 | } else if (hash === 'auth_error!') { 1230 | reactDom.render(stateError, document.body); 1231 | } else { 1232 | const currEpt = utils.getEndpoint(endpoints.endpointList, decodeURIComponent(hash)); 1233 | if (currEpt === null) { 1234 | reactDom.render(endpointNotFound, document.body); 1235 | } else { 1236 | reactDom.render(ce(APIExplorer, { initEpt: currEpt }), document.body); 1237 | } 1238 | } 1239 | }; 1240 | 1241 | const checkCsrf = (state: string): string => { 1242 | if (state === null) return null; 1243 | const div = state.indexOf('!'); 1244 | if (div < 0) return null; 1245 | const csrfToken = state.substring(div + 1); 1246 | if (!utils.checkCsrfToken(csrfToken)) return null; 1247 | return state.substring(0, div); // The part before the CSRF token. 1248 | }; 1249 | 1250 | /* Things that need to be initialized at the start. 1251 | 1. Set up the listener for hash changes. 1252 | 2. Process the initial hash. This only occurs when the user goes through token flow, which 1253 | redirects the page back to the API Explorer website, but with a hash that contains the 1254 | token and some extra state (to check against XSRF attacks). 1255 | */ 1256 | const main = (): void => { 1257 | window.onhashchange = (e: any) => { 1258 | // first one works everywhere but IE, second one works everywhere but Firefox 40 1259 | renderGivenHash(e.newURL ? e.newURL.split('#')[1] : window.location.hash.slice(1)); 1260 | }; 1261 | 1262 | const hashes = utils.getHashDict(); 1263 | if ('state' in hashes) { // completing token flow, and checking the state is OK 1264 | const state = checkCsrf(hashes.state); 1265 | if (state === null) { 1266 | window.location.hash = '#auth_error!'; 1267 | } else { 1268 | utils.putToken(hashes.access_token); 1269 | window.location.href = `${utils.currentURL()}#${state}`; 1270 | } 1271 | } else if ('__ept__' in hashes) { // no token, but an endpoint selected 1272 | renderGivenHash(hashes.__ept__); 1273 | } else { // no endpoint selected: render the intro page 1274 | reactDom.render(introPage, document.body); 1275 | } 1276 | }; 1277 | 1278 | main(); 1279 | --------------------------------------------------------------------------------