├── .gitignore ├── LICENSE ├── README.md ├── static ├── css │ ├── main.css │ └── tachyons.min.css ├── data │ ├── en.json │ └── fr.json ├── favicon.ico ├── img │ ├── .gitignore │ ├── facebook.svg │ ├── industries │ │ ├── dating-applications.svg │ │ ├── fitness-trackers.svg │ │ └── telecommunications.svg │ ├── logo.png │ ├── twitter.svg │ └── whatsapp.svg ├── index-fr.html ├── index-fr.php ├── index.html ├── index.php ├── js │ ├── ami_app │ │ ├── company.js │ │ ├── crypto.js │ │ ├── data_utilities.js │ │ ├── identifiers.js │ │ ├── industry.js │ │ ├── info.js │ │ ├── init.js │ │ ├── request.js │ │ ├── request_helpers.js │ │ ├── router.js │ │ ├── stages.js │ │ ├── stats_helpers.js │ │ └── template_engine.js │ ├── canvasDoc.js │ └── vendor │ │ ├── modernizr-3.7.1.min.js │ │ ├── pdfmake-browserified.min.js │ │ ├── promise-polyfill.min.js │ │ ├── textencoder-polyfill.js │ │ └── webcrypto-shim.min.js └── robots.txt └── stats ├── get_count.php ├── index.php ├── private ├── .htaccess ├── db │ ├── db_connection_template.php │ └── install.sql ├── get_token.php ├── save_request.php ├── validate_company.php ├── validate_hash.php ├── validate_lang.php └── validate_token.php └── process_request.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .editorconfig 3 | .gitattributes 4 | db_connection.php -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 Andrew Hilts 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Access My Info 2 | AMI is a web application that helps people to create legal requests for copies of their personal information from data operators. AMI is a step-by-step wizard that results in the generation of a personalized formal letter requesting access to the information that an operator stores and utilizes about a person. 3 | 4 | **Note**: This is a placeholder application meant to be customized. It is not the code repository for the [AMI Canada](https://accessmyinfo.ca) or [AMI Hong Kong](https://accessmyinfo.hk) implementations. For a full guide on running an Access My Info campaign, check out [https://accessmyinfo.org](https://accessmyinfo.org). 5 | 6 | ## Installation 7 | AMI is for the most part, a static HTML website. Any webserver that can serve HTML files should be capable of serving AMI. 8 | 9 | If you want to track the number of requests created, you will need PHP and MYSQL on your server as well. 10 | 11 | ## Static App 12 | The AMI app lives in the `static` folder. The app is compiled upon page load by JavaScript that looks at the contents of the `js/data.js` file. It then scans `static/index.html` to identify special HTML templates, then does some business logic to populate the templates with the contents of the data model. 13 | 14 | If you wish to track statistics then you can remove the `static/index.html` file and use `static/index.php` instead. 15 | 16 | ### Customization 17 | As mentioned above, this codebase is for a placeholder application. You will see placeholder logos and references to {{YOUR ORG}} that you should replace with your own organization's information. 18 | 19 | You should furthermore review the privacy policy in the footer of the website and make any necessary updates as required by your local jurisdiction. 20 | 21 | ### Data Model 22 | The `data.js` file includes the structured information for the application. 23 | 24 | This includes a list of industries, companies, information categories, and personal identifiers. 25 | 26 | **Industries** are the types of businesses or organizations that users start their request by selecting. Industries must have a `name`, `id`, `description`, and `icon`. 27 | 28 | **Companies** are the organizations to which the user sends a request. Companies must have a `name`, `id`, `logo`, `contact`, and be assigned an `industry`. Company contact information must at least have the following attributes: `title`, `has_mail`, `has_email`. `has_mail` and `has_email` should be set to true or false. One of them must be true at least. If `has_mail` is true, then the company contact object must also have `address1`, `address2`, `city`, `region`, `postalcode`, and `country` defined. If `has_email` is true, then `email` must also be set and include a valid email address. 29 | 30 | **Info Categories** are the types of personal information that companies might hold on people, and that users can include in their request letters. Each info categories must have the following: `name`, `id`, `description`, and be assigned one or more `industries`. `industries` is an array, so even if one industry is assigned, it would have to look like this `["industry1"]`. 31 | 32 | **Personal Identifiers** are included in a request so the company can find the requester in their records. Personal identifiers must have the following attributes: `name`, `id`, `description`, and be assigned one or more `industries`. `industries` is an array, so even if one industry is assigned, it would have to look like this `["industry1"]`. Personal identifiers may also include an `options` attribute. This will allow the user to choose from a list of options for a particular identifier (like province of residence). `options` must be an array of objects, with each object having the `id` and `name` attributes. For example: `"options": [{"id": "first", "name": "First Option"}]` 33 | 34 | ### Editing the frontend 35 | There are lots of comments throughout `index.php` and `js/ami/*.js` files to help you along with customizing the look and feel. Have fun! 36 | 37 | If you don't want to use stats or don't have a PHP webserver, simply rename `index.php` to `index.html` and remove all `` stuff (top of file and near bottom), following the instructions in the comments. 38 | 39 | ## Statistics 40 | To track statistics, you will need a PHP web server with a MYSQL database. 41 | 42 | The statistics code lives in the `stats` folder. 43 | 44 | `index.php` displays a simple table of how many stats each company has received. 45 | 46 | `process_request.php` is the file that the frontend sends its statistics form data to. You will have to configure `index.php` to point to the right URL, following the comments in the last script block at the bottom of the page. 47 | 48 | It's best practice to move the `private` folder out of the webroot or add an `.htaccess` file to the folder to make it private. These files should not be accessible on the public internet for security reasons. 49 | 50 | ### Installation 51 | Create a new MYSQL database for tracking stats. 52 | 53 | Copy `stats/private/db/db_connection_template.php` to`stats/private/db/db_connection.php`, and edit the new file to point to that new database, and edit the credentials as needed. 54 | 55 | Execute the `install.sql` command in PHPMyAdmin or through command line. THis will create the required database tables. 56 | 57 | 58 | -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width:33em; 3 | } 4 | .mxh-4 { 5 | max-height:4rem; 6 | } 7 | .btn { 8 | /*display: inline-block;*/ 9 | border-radius: 0.5rem; 10 | padding: 0.625rem 0.75rem 0.5rem 0.75rem; 11 | text-decoration: none; 12 | line-height: 1.5; 13 | min-width: 4rem; 14 | text-align: center; 15 | cursor: pointer; 16 | } 17 | .btn-select { 18 | text-align: left; 19 | border-radius: 0; 20 | border: 2px solid #F2F2F2; 21 | background-color: #fff; 22 | } 23 | .btn-media-select { 24 | border: 2px solid #00449E; 25 | } 26 | .btn-previous, .btn-secondary { 27 | border-style: none; 28 | border: 0; 29 | background-color: #999; 30 | color: #fff; 31 | } 32 | .btn-primary { 33 | border-style: none; 34 | border: 0; 35 | background-color: #00449E; 36 | color: #fff; 37 | } 38 | .btn-social { 39 | border-style: none; 40 | border: 0; 41 | background-color: #00449E; 42 | color: #fff; 43 | min-width: 0; 44 | } 45 | .btn-disabled { 46 | border-style: none; 47 | border: 0; 48 | background-color: #ccc; 49 | color: #999; 50 | } 51 | .fill-white { 52 | fill: white; 53 | } 54 | #personal_identifiers input { 55 | margin-top: 0.25rem; 56 | margin-bottom: 0.5rem; 57 | display: block; 58 | width: 100%; 59 | } 60 | .note { 61 | display: block; 62 | padding: 0.25em 0.5em; 63 | margin-top: 0.25em; 64 | font-weight: normal; 65 | background: #e69999; 66 | } 67 | .display-flex-center { display: flex; align-items: center; justify-content: space-between;} 68 | .flexgrow-1 { 69 | flex-grow:1; 70 | } 71 | .flexgrow-2 { 72 | flex-grow:2; 73 | } 74 | .flexy { 75 | display:flex; 76 | justify-content: space-between; 77 | align-items: center; 78 | } 79 | .flexy-big { 80 | flex-grow: 4 81 | } 82 | .flexy-small { 83 | flex-grow: 1; 84 | } 85 | 86 | 87 | @media screen and (min-width: 48em) and (max-width: 64em) { 88 | html {font-size: 105%;} 89 | } 90 | @media screen and (min-width: 64em) and (max-width: 86em) { 91 | html {font-size: 110.25%;} 92 | } 93 | @media screen and (min-width: 86em) and (max-width: 108em) { 94 | html {font-size: 115.76%;} 95 | } 96 | @media screen and (min-width: 108em) and (max-width: 130em) { 97 | html {font-size: 121.55%;} 98 | } 99 | @media screen and (min-width: 130em) and (max-width: 152em) { 100 | html {font-size: 134.01%;} 101 | } 102 | @media screen and (min-width: 152em) { 103 | html {font-size: 140.71%;} 104 | } 105 | .bg-darkestred { 106 | background-color: #9d0003; 107 | } 108 | .height-xlarger { 109 | min-height: 5rem; 110 | } 111 | .note { 112 | display:block; 113 | font-size: 0.6em; 114 | padding:0.25em 0.5em; 115 | margin-top:0.25em; 116 | font-weight: normal; 117 | background: #e69999; 118 | } 119 | @media screen and (min-width: 48em){ 120 | #stageNav { 121 | border-radius: 1em; 122 | overflow:hidden; 123 | } 124 | } 125 | #stageNav li { 126 | width:20%; 127 | } 128 | #stageNav li a { 129 | width:100%; 130 | text-align:center; 131 | } 132 | #stageNav .active { 133 | color:white; 134 | background:#00449E; 135 | cursor: pointer; 136 | position:relative; 137 | } 138 | #stageNav .active:after { 139 | content: " "; 140 | display: block; 141 | width: 0; 142 | height: 0; 143 | border-top: 1.6rem solid transparent; 144 | border-bottom: 1.6rem solid transparent; 145 | border-left: 0.8rem solid #00449E; 146 | position: absolute; 147 | top: 50%; 148 | margin-top: -1.6rem; 149 | left: 100%; 150 | z-index: 2; 151 | } 152 | #stageNav li:last-child a:after { 153 | display:none; 154 | } 155 | #stageNav .previous { 156 | color:white; 157 | background:#001B44; 158 | cursor: pointer; 159 | } 160 | #stageNav .future { 161 | color:white; 162 | cursor: pointer; 163 | background:#357EDD; 164 | } 165 | #stageNav .disabled { 166 | color: #777; 167 | cursor: default; 168 | background:#b7cbe6; 169 | } 170 | 171 | #email_instructions { 172 | display: block; 173 | padding: 0; 174 | list-style: none; 175 | overflow: hidden; 176 | counter-reset: numList; 177 | } 178 | #email_instructions li { 179 | position: relative; 180 | padding-left:32px; 181 | } 182 | #email_instructions li:before { 183 | counter-increment: numList; 184 | content: counter(numList); 185 | 186 | float: left; 187 | position: absolute; 188 | left: 0; 189 | 190 | font: bold 16px sans-serif; 191 | text-align: center; 192 | color: #fff; 193 | line-height: 24px; 194 | 195 | width: 24px; height: 24px; 196 | background: #00449E; 197 | 198 | -moz-border-radius: 999px; 199 | border-radius: 999px 200 | } 201 | .form-item { 202 | margin-bottom: 1em; 203 | display: block; 204 | } 205 | .form-item input { 206 | margin-top: 0.25rem; 207 | margin-bottom: 0.5rem; 208 | display: block; 209 | width: 100%; 210 | } -------------------------------------------------------------------------------- /static/data/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "request_subject_line": "Formal request for access to my personal information", 3 | "request_pdf_filename": "Formal_request_for_access_to_my_personal_information.pdf", 4 | "industries": [ 5 | { 6 | "id": "telco", 7 | "name": "Telecommunications", 8 | "icon": "img/industries/telecommunications.svg", 9 | "description": "Your phone call records, web browsing history, geolocation, and device identifiers." 10 | }, 11 | { 12 | "id": "fitness", 13 | "name": "Fitness Trackers", 14 | "icon": "img/industries/fitness-trackers.svg", 15 | "description": "Your heartbeat, sleeping patterns, diet, weight, walking habits, and general health." 16 | }, 17 | { 18 | "id": "dating", 19 | "name": "Dating Applications", 20 | "icon": "img/industries/dating-applications.svg", 21 | "description": "Your personality traits, sexual preferences, dating history, and other lifestyle information." 22 | } 23 | ], 24 | "companies": [ 25 | { 26 | "name":"Telco 1", 27 | "id":"Telco1", 28 | "logo":"img/industries/telecommunications.svg", 29 | "contact":{ 30 | "title":"The Privacy Manager, Telco 1", 31 | "has_mail":true, 32 | "has_email":true, 33 | "address1":"3rd Floor, Telco1 HQ, P.O. Box 5435", 34 | "city":"Telco City", 35 | "region":"TC", 36 | "postalcode":"123456", 37 | "country":"", 38 | "email":"PrivacyManager@telco1.telco" 39 | }, 40 | "industry":"telco" 41 | }, 42 | { 43 | "name":"Telco 2", 44 | "id":"Telco2", 45 | "logo":"img/industries/telecommunications.svg", 46 | "contact":{ 47 | "title":"The Privacy Manager, Telco 2", 48 | "has_mail":true, 49 | "has_email":true, 50 | "address1":"5th Floor, Telco2 HQ, P.O. Box 6546", 51 | "city":"Telco City", 52 | "region":"TC", 53 | "postalcode":"123454", 54 | "country":"", 55 | "email":"PrivacyManager@telco2.telco" 56 | }, 57 | "industry":"telco" 58 | }, 59 | { 60 | "name":"Dating Site 1", 61 | "id":"dating1", 62 | "logo":"img/industries/dating-applications.svg", 63 | "contact":{ 64 | "title":"Privacy Officer, Dating Site 1", 65 | "has_mail":true, 66 | "has_email":true, 67 | "address1":"8300 Dating Avenue, Suite 800", 68 | "city":"Dating City", 69 | "region":"DC", 70 | "postalcode":"542534", 71 | "country":"Romanceland", 72 | "email":"privacy@datingsite1.com" 73 | }, 74 | "industry":"dating" 75 | }, 76 | { 77 | "name":"Fitness Company 1", 78 | "id":"fitness1", 79 | "logo":"img/industries/fitness-trackers.svg", 80 | "contact":{ 81 | "title":"Chief Privacy Officer", 82 | "has_mail":true, 83 | "has_email":true, 84 | "address1":"2050 FitLand West", 85 | "city":"Fit City", 86 | "region":"FC", 87 | "postalcode":"23454645", 88 | "country":"", 89 | "email":"privacy.officer@fitco1.com" 90 | }, 91 | "industry":"fitness" 92 | } 93 | ], 94 | "info_categories": [ 95 | { 96 | "name": "Call logs", 97 | "id": "call_logs", 98 | "description": "E.g. numbers dialed, times and dates of calls, call durations, routing information, and any geolocational or cellular tower information associated with the calls)", 99 | "industries": ["telco"] 100 | }, 101 | { 102 | "name": "Mobile app data", 103 | "id": "app_data", 104 | "description": "Information collected about me, or persons/devices associated with my account, using one of your company’s mobile device applications", 105 | "industries": ["telco","dating","fitness"] 106 | }, 107 | { 108 | "name": "Geolocation data", 109 | "id": "geo_data", 110 | "description": "collected about me, my devices, and/or associated with my account (e.g. GPS information, cell tower information)", 111 | "industries": ["telco","dating","fitness"] 112 | }, 113 | { 114 | "name": "IP address logs", 115 | "id": "ip_logs", 116 | "description": "associated with me, my devices, and/or my account (e.g. IP addresses assigned to my devices/router, IP addresses or domain names of sites I visit and the times, dates, and port numbers)", 117 | "industries": ["telco","dating","fitness"] 118 | }, 119 | { 120 | "name": "Disclosures to third parties", 121 | "id": "disclosures", 122 | "description": "Any information about disclosures of my personal information, or information about my account or devices, to other parties, including law enforcement and other state agencies", 123 | "industries": ["telco","dating","fitness"] 124 | }, 125 | { 126 | "name": "Text & multimedia messages", 127 | "id": "sms_mms", 128 | "description": "(sent and received, including date, time, and recipient information)", 129 | "industries": ["telco"] 130 | }, 131 | { 132 | "name": "Subscriber information", 133 | "id": "subscriber_info", 134 | "description": "that you store about me, my devices, and/or my account", 135 | "industries": ["telco","dating","fitness"] 136 | }, 137 | { 138 | "name": "Other", 139 | "id": "other", 140 | "description": "Any additional kinds of information that you have collected, retained, or derived from the telecommunications services or devices that I, or someone associated with my account, have transmitted or received using your company’s services", 141 | "industries": ["telco"] 142 | }, 143 | { 144 | "name": "Any additional kinds of information", 145 | "id": "other2", 146 | "description": "that you have collected, retained, or derived from the mobile or website services you provide, including by not limited to: data or records collected using my camera or from my camera roll; social networking information; data collected or retained derived from my microphone; or communications between myself and other users; contact book information;", 147 | "industries": ["dating"] 148 | }, 149 | { 150 | "name": "Personally identifying information", 151 | "id": "pii", 152 | "description": "that is unique to me, my devices, and/or my account, such as name, email addresses, phone numbers, responses to relationship questions, or device identifiers;", 153 | "industries": ["dating"] 154 | }, 155 | { 156 | "name": "Lifestyle information ", 157 | "id": "lifestyle", 158 | "description": "that you may have about me, such as drinking habits or sexual preference information.", 159 | "industries": ["dating"] 160 | }, 161 | { 162 | "name": "Health and fitness data", 163 | "id": "health", 164 | "description": "including all records of my step activity, heart rate, sleep patterns, food intake.", 165 | "industries": ["fitness"] 166 | }, 167 | { 168 | "name": "Any additional kinds of information", 169 | "id": "other3", 170 | "description": "that you have collected, retained, or derived from the mobile or website services your provide, or with the fitness-related device your company produces that I use", 171 | "industries": ["fitness"] 172 | } 173 | ], 174 | "personal_identifiers": [ 175 | { 176 | "name": "First Name", 177 | "id": "firstname", 178 | "description": "Your first name", 179 | "industries": ["telco", "fitness","dating"] 180 | }, 181 | { 182 | "name": "Last Name", 183 | "id": "lastname", 184 | "description": "Your last name", 185 | "industries": ["telco", "fitness","dating"] 186 | }, 187 | { 188 | "name": "Email", 189 | "id": "email", 190 | "description": "Your email address", 191 | "industries": ["telco", "fitness","dating"] 192 | }, 193 | { 194 | "name": "Telephone", 195 | "id": "telephone", 196 | "description": "Your phone number.", 197 | "industries": ["telco","dating"] 198 | }, 199 | { 200 | "name": "Account Number", 201 | "id": "account_no", 202 | "description": "Your account number", 203 | "industries": ["telco"] 204 | }, 205 | { 206 | "name": "Username", 207 | "id": "username", 208 | "description": "Your username", 209 | "industries": ["dating"] 210 | }, 211 | { 212 | "name": "Address 1", 213 | "id": "address1", 214 | "description": "First line of your address", 215 | "industries": ["telco"] 216 | }, 217 | { 218 | "name": "Address 2", 219 | "id": "address2", 220 | "description": "Second line of your address", 221 | "industries": ["telco"] 222 | }, 223 | { 224 | "name": "City", 225 | "id": "city", 226 | "description": "City you reside in", 227 | "industries": ["telco"] 228 | }, 229 | { 230 | "name": "Province", 231 | "id": "province", 232 | "description": "Province you reside in", 233 | "options": [ 234 | { 235 | "id": "AB", 236 | "name": "Alberta" 237 | }, 238 | { 239 | "id": "BC", 240 | "name": "British Columbia" 241 | }, 242 | { 243 | "id": "QC", 244 | "name": "Québec" 245 | }, 246 | { 247 | "id": "MB", 248 | "name": "Manitoba" 249 | }, 250 | { 251 | "id": "NB", 252 | "name": "New Brunswick" 253 | }, 254 | { 255 | "id": "NL", 256 | "name": "Newfoundlan and Labrador" 257 | }, 258 | { 259 | "id": "NS", 260 | "name": "Nova Scotia" 261 | }, 262 | { 263 | "id": "NT", 264 | "name": "Northwest Territories" 265 | }, 266 | { 267 | "id": "NU", 268 | "name": "Nunavut" 269 | }, 270 | { 271 | "id": "ON", 272 | "name": "Ontario" 273 | }, 274 | { 275 | "id": "PE", 276 | "name": "Prince Edward Island" 277 | }, 278 | { 279 | "id": "QC", 280 | "name": "Québec" 281 | }, 282 | { 283 | "id": "SK", 284 | "name": "Saskatchewan" 285 | }, 286 | { 287 | "id": "YK", 288 | "name": "Yukon" 289 | } 290 | ], 291 | "industries": ["telco"] 292 | }, 293 | { 294 | "name": "Postal Code", 295 | "id": "postalcode", 296 | "description": "Postal Code", 297 | "industries": ["telco"] 298 | } 299 | ] 300 | } 301 | -------------------------------------------------------------------------------- /static/data/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "request_subject_line": "Requête formelle d’accès à mes renseignements personnels", 3 | "request_pdf_filename": "Requete_formelle_d_acces_a_mes_renseignements_personnels.pdf", 4 | "industries": [ 5 | { 6 | "id": "telco", 7 | "name": "Télécommunications", 8 | "icon": "img/industries/telecommunications.svg", 9 | "description": "Votre journal d’appels, votre historique de navigation web, votre géolocalisation et les identifiants de votre appareil." 10 | }, 11 | { 12 | "id": "fitness", 13 | "name": "Appareils de suivi de la forme physique", 14 | "icon": "img/industries/fitness-trackers.svg", 15 | "description": "Votre rythme cardiaque, vos habitudes de sommeil, votre régime alimentaire, votre poids, vos itinéraires de marche et votre santé générale." 16 | }, 17 | { 18 | "id": "dating", 19 | "name": "Applications de rencontres", 20 | "icon": "img/industries/dating-applications.svg", 21 | "description": "Vos traits de personnalité, vos préférences sexuelles, votre historique de rencontres et d’autres informations sur votre style de vie." 22 | } 23 | ], 24 | "companies": [ 25 | { 26 | "name":"Telco 1", 27 | "id":"Telco1", 28 | "logo":"img/industries/telecommunications.svg", 29 | "contact":{ 30 | "title":"The Privacy Manager, Telco 1", 31 | "has_mail":true, 32 | "has_email":true, 33 | "address1":"3rd Floor, Telco1 HQ, P.O. Box 5435", 34 | "city":"Telco City", 35 | "region":"TC", 36 | "postalcode":"123456", 37 | "country":"", 38 | "email":"PrivacyManager@telco1.telco" 39 | }, 40 | "industry":"telco" 41 | }, 42 | { 43 | "name":"Telco 2", 44 | "id":"Telco2", 45 | "logo":"img/industries/telecommunications.svg", 46 | "contact":{ 47 | "title":"The Privacy Manager, Telco 2", 48 | "has_mail":true, 49 | "has_email":true, 50 | "address1":"5th Floor, Telco2 HQ, P.O. Box 6546", 51 | "city":"Telco City", 52 | "region":"TC", 53 | "postalcode":"123454", 54 | "country":"", 55 | "email":"PrivacyManager@telco2.telco" 56 | }, 57 | "industry":"telco" 58 | }, 59 | { 60 | "name":"Dating Site 1", 61 | "id":"dating1", 62 | "logo":"img/industries/dating-applications.svg", 63 | "contact":{ 64 | "title":"Privacy Officer, Dating Site 1", 65 | "has_mail":true, 66 | "has_email":true, 67 | "address1":"8300 Dating Avenue, Suite 800", 68 | "city":"Dating City", 69 | "region":"DC", 70 | "postalcode":"542534", 71 | "country":"Romanceland", 72 | "email":"privacy@datingsite1.com" 73 | }, 74 | "industry":"dating" 75 | }, 76 | { 77 | "name":"Fitness Company 1", 78 | "id":"fitness1", 79 | "logo":"img/industries/fitness-trackers.svg", 80 | "contact":{ 81 | "title":"Chief Privacy Officer", 82 | "has_mail":true, 83 | "has_email":true, 84 | "address1":"2050 FitLand West", 85 | "city":"Fit City", 86 | "region":"FC", 87 | "postalcode":"23454645", 88 | "country":"", 89 | "email":"privacy.officer@fitco1.com" 90 | }, 91 | "industry":"fitness" 92 | } 93 | ], 94 | "info_categories": [ 95 | { 96 | "name": "Journal d’appels", 97 | "id": "call_logs", 98 | "description": "(i.e. numéros composés, heure, date et durée des appels, information sur l’acheminement des appels, et toute information sur la géolocalisation ou sur les antennes-relais de téléphonie mobile associée aux appels)", 99 | "industries": ["telco"] 100 | }, 101 | { 102 | "name": "Données des applications mobiles", 103 | "id": "app_data", 104 | "description": "Renseignements recueillis sur moi ou sur les personnes/appareils associés à mon compte d’utilisateur, utilisant une des applications mobiles de votre entreprise.", 105 | "industries": ["telco","dating","fitness"] 106 | }, 107 | { 108 | "name": "Données de géolocalisation", 109 | "id": "geo_data", 110 | "description": "recueillies à mon sujet et au sujet de mes appareils, et/ou des associées à mon compte d’utilisateur (i.e. information GPS, information sur les antennes-relais de téléphonie mobile)", 111 | "industries": ["telco","dating","fitness"] 112 | }, 113 | { 114 | "name": "Journaux d’adresses IP", 115 | "id": "ip_logs", 116 | "description": "associées à moi, à mes appareils et/ou à mon compte d’utilisateur (i.e. adresses IP assignées à mes appareils/routeurs, adresses IP ou noms de domaine des sites que je visite, heure et date des visites ainsi que numéros de port)", 117 | "industries": ["telco","dating","fitness"] 118 | }, 119 | { 120 | "name": "Divulgation à des tiers-parties", 121 | "id": "disclosures", 122 | "description": "Toute information concernant la divulgation de mes renseignements personnels, ou de renseignements concernant mon compte d’utilisateur ou mes appareils, à d’autres parties, incluant des organismes d’application de la loi et des agences gouvernementales", 123 | "industries": ["telco","dating","fitness"] 124 | }, 125 | { 126 | "name": "Messages textes et multimédias", 127 | "id": "sms_mms", 128 | "description": "(envoyés et reçus, incluant la date et l'heure du message et l'information sur le destinataire)", 129 | "industries": ["telco"] 130 | }, 131 | { 132 | "name": "Information d’abonné", 133 | "id": "subscriber_info", 134 | "description": "que vous enregistrez à mon sujet et au sujet de mes appareils et/ou de mon compte d’utilisateur", 135 | "industries": ["telco","dating","fitness"] 136 | }, 137 | { 138 | "name": "Autre", 139 | "id": "other", 140 | "description": "Tout autre type de renseignement que vous avez recueilli, conservé ou dérivé des services de télécommunications ou des appareils que j’utilise ou qu’une personne associée à mon compte utilise.", 141 | "industries": ["telco"] 142 | }, 143 | { 144 | "name": "Tout autre type de renseignement", 145 | "id": "other2", 146 | "description": "que vous avez recueilli, conservé ou dérivé dans la fourniture de votre service mobile ou de votre site Internet, incluant mais non limité à : données ou enregistrements recueillis en utilisant ma caméra ou l’album de ma caméra, informations liées aux médias sociaux, données recueillies ou conservées dérivées de mon microphone, communications entre moi et d’autres utilisateurs, informations de mon carnet d’adresses;", 147 | "industries": ["dating"] 148 | }, 149 | { 150 | "name": "Informations personnellement identifiables", 151 | "id": "pii", 152 | "description": "à mon sujet et au sujet de mes appareils et/ou mon compte, telles que mon nom, mes courriels, mes numéros de téléphones, mes réponses à des questions sur mes relations personnelles ou les identifiants de mes appareils;", 153 | "industries": ["dating"] 154 | }, 155 | { 156 | "name": "Renseignements sur mon style de vie", 157 | "id": "lifestyle", 158 | "description": "que vous pourriez détenir, comme mes habitudes de consommation d’alcool ou mes préférences sexuelles.", 159 | "industries": ["dating"] 160 | }, 161 | { 162 | "name": "Santé et données sur ma condition physique", 163 | "id": "health", 164 | "description": "incluant les journaux de mes activités de marche, mon rythme cardiaque, mes habitudes de sommeil et mon régime alimentaire.", 165 | "industries": ["fitness"] 166 | }, 167 | { 168 | "name": "Tout autre type de renseignement", 169 | "id": "other3", 170 | "description": " que vous avez recueilli, conservé ou dérivé de votre service mobile ou de votre site Internet, ou de l’appareil de suivi de la forme physique que votre entreprise fabrique et que j’utilise", 171 | "industries": ["fitness"] 172 | } 173 | ], 174 | "personal_identifiers": [ 175 | { 176 | "name": "Prénom", 177 | "id": "firstname", 178 | "description": "Your first name", 179 | "industries": ["telco", "fitness","dating"] 180 | }, 181 | { 182 | "name": "Nom", 183 | "id": "lastname", 184 | "description": "Your last name", 185 | "industries": ["telco", "fitness","dating"] 186 | }, 187 | { 188 | "name": "Adresse courriel", 189 | "id": "email", 190 | "description": "Your email address", 191 | "industries": ["telco", "fitness","dating"] 192 | }, 193 | { 194 | "name": "Numéro de téléphone", 195 | "id": "telephone", 196 | "description": "Your phone number.", 197 | "industries": ["telco","dating"] 198 | }, 199 | { 200 | "name": "Numéro de compte", 201 | "id": "account_no", 202 | "description": "Your account number", 203 | "industries": ["telco"] 204 | }, 205 | { 206 | "name": "Nom d’utilisateur", 207 | "id": "username", 208 | "description": "Your username", 209 | "industries": ["dating"] 210 | }, 211 | { 212 | "name": "Adresse 1", 213 | "id": "address1", 214 | "description": "First line of your address", 215 | "industries": ["telco"] 216 | }, 217 | { 218 | "name": "Adresse 2", 219 | "id": "address2", 220 | "description": "Second line of your address", 221 | "industries": ["telco"] 222 | }, 223 | { 224 | "name": "Ville", 225 | "id": "city", 226 | "description": "City you reside in", 227 | "industries": ["telco"] 228 | }, 229 | { 230 | "name": "Province", 231 | "id": "province", 232 | "description": "Province you reside in", 233 | "options": [ 234 | { 235 | "id": "AB", 236 | "name": "Alberta" 237 | }, 238 | { 239 | "id": "BC", 240 | "name": "Colombie-Britannique" 241 | }, 242 | { 243 | "id": "QC", 244 | "name": "Québec" 245 | }, 246 | { 247 | "id": "MB", 248 | "name": "Manitoba" 249 | }, 250 | { 251 | "id": "NB", 252 | "name": "Nouveau-Brunswick" 253 | }, 254 | { 255 | "id": "NL", 256 | "name": "Terre-Neuve-et-Labrador" 257 | }, 258 | { 259 | "id": "NS", 260 | "name": "Nouvelle-Écosse" 261 | }, 262 | { 263 | "id": "NT", 264 | "name": "Territoires du Nord-Ouest" 265 | }, 266 | { 267 | "id": "NU", 268 | "name": "Nunavut" 269 | }, 270 | { 271 | "id": "ON", 272 | "name": "Ontario" 273 | }, 274 | { 275 | "id": "PE", 276 | "name": "Île-du-Prince-Édouard" 277 | }, 278 | { 279 | "id": "QC", 280 | "name": "Québec" 281 | }, 282 | { 283 | "id": "SK", 284 | "name": "Saskatchewan" 285 | }, 286 | { 287 | "id": "YK", 288 | "name": "Yukon" 289 | } 290 | ], 291 | "industries": ["telco"] 292 | }, 293 | { 294 | "name": "Code Postal", 295 | "id": "postalcode", 296 | "description": "Postal Code", 297 | "industries": ["telco"] 298 | } 299 | ] 300 | } -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citizenlab/ami/069bfb2287e777240cb57e443a3c0c6654ffb5c3/static/favicon.ico -------------------------------------------------------------------------------- /static/img/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citizenlab/ami/069bfb2287e777240cb57e443a3c0c6654ffb5c3/static/img/.gitignore -------------------------------------------------------------------------------- /static/img/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/industries/dating-applications.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /static/img/industries/fitness-trackers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 18 | 19 | 20 | 23 | 24 | 25 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /static/img/industries/telecommunications.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 19 | 20 | 21 | 25 | 26 | 27 | 32 | 33 | 34 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citizenlab/ami/069bfb2287e777240cb57e443a3c0c6654ffb5c3/static/img/logo.png -------------------------------------------------------------------------------- /static/img/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/whatsapp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | Access My Info 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
This is an insecure web server. Access My Info should only be hosted using HTTPS.
33 | 38 | 39 | 51 | 52 | 63 | 97 | 98 |
99 | 109 |
110 |

Welcome to Access My Info!

111 |

What do companies know about you? What do they keep on file? Who do they share it with? Organizations are required by Canadian privacy laws to disclose this information to their customers upon request. We can help with that.

112 |
113 |

Click an option below to start your request:

114 |
    115 | 133 |
  • 134 |
    135 | 153 | Industry icon 154 |
    155 |
    156 | 169 |
    170 | 171 |

    Industry Name

    172 | 173 |
    Industry Description
    174 |
    175 |
  • 176 |
177 |

You may make multiple requests with our website (but one at a time!).

178 |
179 |
180 | 181 | 189 |
190 |

Select your service provider

191 |

Begin your request by selecting a company that provides you a service.

192 |
193 |
    194 | 195 |
  • 196 |
    197 | 198 | Company icon 199 |
    200 |
    201 | 202 |
    203 | 204 |

    Company Name

    205 |
    206 |
  • 207 |
208 |
209 |
210 |

Help:My company isn't listed

211 |
212 |
213 |

Add custom company

214 | 217 |
218 | Company Privacy Contact 219 |

Please provide the contact information for the person responsible for interacting with the public regarding customer privacy. Usually this information can be found on the company's privacy policy.

220 |

Provide either a full mailing address or an email address.

221 | 223 | 225 | 227 | 229 | 231 | 233 | 235 | 237 |
238 |
239 |
240 |
241 |
242 | 243 | 244 |
245 |
246 | 247 | 256 |
257 |

What data do you want to access?

258 |

Make inquiries about how your data is collected, used, shared and stored.

259 |
260 | 261 |

Data requested from Org

262 | This list is meant to be exhaustive. 263 | 264 | 265 | Org 266 | may not retain some of these items. 267 |
268 | 269 |
270 |
271 | 272 |
273 | 284 |
285 |
286 |
287 |
288 | 289 |
290 | 293 |
294 |
295 | 296 |
297 |
298 | 299 | 308 |
309 |

Identifying information

310 |

Enter your information so 311 | 312 | 313 | Org 314 | 315 | can identify you in their records.

316 |

Access My Info will not collect or store any of the personal information below.

317 |
318 |
319 | 320 |
321 |
322 | 323 |
324 | 330 |
331 |
332 |
333 | 334 |
335 |
336 |

I consent and authorize:

337 |
338 | 339 |
340 |
341 |
342 | 343 |
344 | 347 |
348 |
349 |
350 |
351 | 352 | 362 |
363 | 364 |

Your request is ready

365 |

Your letter to 366 | 367 | Org 368 | has been successfully generated by our system. Now, it's up to you to send it!

369 |

Read over the letter carefully, then follow the instructions below.

370 | 371 | 372 |
373 |
374 | 375 |
376 |

377 | today 378 |

379 |
380 | 381 |
382 |
383 |
384 | Privacy Officer 385 |
386 |
387 | Privacy Officer 388 |
389 |
390 | Privacy Officer 391 |
392 |
393 | Privacy Officer 394 |
395 |
396 | Privacy Officer 397 |
398 |
399 | Privacy Officer 400 |
401 |
402 | Privacy Officer 403 |
404 |
405 |
406 | 407 |

I am a user of your telecommunications service, and am interested in both learning more about your data management practices and about the kinds of personal information that you maintain and retain about me. So this is a request to access my personal data under’ Principle 4.9 of Schedule 1 and section 8 Canada’s federal privacy legislation, the Personal Information Protection and Electronic Documents Act (PIPEDA).

408 | 409 |

I am requesting a copy of all records which contain my personal information from your organization.

410 | 411 |

The following is a non-exclusive listing of all information that your organization may hold about me, including the following:

412 | 413 | 414 |
    415 |
  • 416 | Info Category Name: 417 | Info Category Description 418 |
  • 419 |
420 | 421 | 422 |

If your organization has other information in addition to these items, I formally request access to that as well. If your service includes a data export tool, please direct me to it, and ensure that in your response to this letter, you provide all information associated with me that is not included in the output of this tool. Please ensure that you include all information that is directly associated with my name, phone number, e-mail, or account number, as well as any other account identifiers that your company may associate with my personal information.

423 | 424 |

You are obligated to provide copies at a free or minimal cost within thirty (30) days in receipt of this message. If you choose to deny this request, you must provide a valid reason for doing so under Canada’s PIPEDA. Ignoring a written request is the same as refusing access. See the guide from the Office of the Privacy Commissioner at: http://www.priv.gc.ca/information/guide_e.asp#014. The Commissioner is an independent oversight body that handles privacy complaints from the public.

425 | 426 |

Please let me know if your organization requires additional information from me before proceeding with my request.

427 | 428 |

Here is information that may help you identify my records:

429 | 430 | 431 |
    432 |
  • 433 | Info Category Name: 434 | Info Category Description 435 |
  • 436 |
437 | 438 |

Sincerely,

439 | 440 |

441 | 442 | Information Category Information Category 443 | 444 |

445 |
446 |
447 | 448 | 449 | 450 |

How would you like to send your letter?

451 |

This company currently only accepts requests by postal mail.

452 |
453 |

Postal Mail

454 |
455 |

Use the button below to create a PDF of your letter. Then print it and mail it to:

456 |
457 | 458 |
459 | 460 | Privacy Officer 461 | 462 | 463 | Privacy Officer 464 | 465 | 466 | Privacy Officer 467 | 468 | 469 | Privacy Officer 470 | 471 | 472 | Privacy Officer 473 | 474 | 475 | Privacy Officer 476 | 477 | 478 | Privacy Officer 479 | 480 |
481 | 482 |
483 | 484 |
485 | 488 |
489 |
490 |
491 |

This company currently only accepts requests by email.

492 |
493 |

Email

494 |
495 |
496 | 497 |

Follow these five easy steps to send your email:

498 |
    499 |
  1. 500 |
    501 | Copy your request letter to your system clipboard. 502 |
    503 |
    504 | 505 |
    506 |
  2. 507 |
  3. 508 |
    509 | Open your email client. 510 |
    511 |
    512 | 517 | 522 |
    523 |
  4. 524 |
  5. 525 |
    Review the to and subject fields.
    526 |
    527 | To: 528 | 529 |
    530 | 531 | Privacy Officer 532 |
    533 |
    534 | 535 | Subject: Formal request for Access to my personal information 536 | 537 |
    538 |
  6. 539 |
  7. Paste the letter into where the email content should go.
  8. 540 |
  9. Send your message!
  10. 541 |
542 |
543 |
544 |
545 | 546 |
547 | 548 |

That's it! Thank you for participating in the Access My Info project.

549 |

Spread the word about our project:

550 | 551 | 562 |
563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 |
575 |
576 | 577 |
578 | 579 | 587 |
588 |

This version of Access my Info is supported by

589 |

Logo

590 |

Questions? contact email@yourorg.ca to learn more.

591 | 592 |

About this project

593 |

Access My Info (AMI) is a web application that enables you to find out what a variety of different companies know about you. It guides you via a step-by-step wizard to generate a formal letter that requests access to your personal information. This letter can then be sent via postal mail or email to the respective company’s privacy officer.

594 |
595 |
596 |
597 |
598 |

Privacy Policy: This service does not collect any of the personal information you provide. Your information is used only to generate a letter, which is done entirely in your web browser. When your web browser communicates with this service, the web server that hosts the service logs a record of that event. These logs include the IP address used to access each resource required for the service, the date & time of access, and several other non-identifying metadata fields. Other web pages linked to from this service will be governed by different policies. 599 |

Disclaimer: This is a research and educational tool and is meant for informational purposes only. This service does not provide legal advice. You are solely responsible for your use of this service and any resulting consequences. {{YOUR ORG}} make no claims, promises, or guarantees about the accuracy, completeness, or adequacy of the information contained in this document. This software is offered as-is, with no warranty. Nothing herein should be used as a substitute for the legal advice of competent counsel.

600 |

This work is based on Access My Info by the Citizen Lab and Open Effect and licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

601 |
602 |
603 | 612 |
613 | 614 | 615 | 616 | 617 | 618 | Server Response: 619 |
620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 646 | 647 | 648 | 649 | -------------------------------------------------------------------------------- /static/js/ami_app/company.js: -------------------------------------------------------------------------------- 1 | amiApp.company_list_controller = function(targetEl, customEl, customBtnEl){ 2 | var self = this; 3 | self.activeStage = "company"; 4 | location.hash = "#org"; 5 | targetEl.innerHTML = ""; 6 | 7 | // Create elements representing each company belonging to the selected industry. 8 | for (var i = 0; i < self.request.industry.companies.length; i++) { 9 | el = self.createFromTemplate("company_select_template", self.request.industry.companies[i]); 10 | // If the user has gone back a stage, make sure to highlight the company they had selected 11 | if(self.request.industry.companies[i] === self.request.company){ 12 | el.classList.add("btn-media-select"); 13 | } 14 | // Clicking a company element will result in selecting that company for the request. 15 | el.onclick = function(){ 16 | var id = this.querySelector('*[ami_template_value_container="id"]').innerHTML; 17 | self.selectCompany(id); 18 | } 19 | targetEl.appendChild(el); 20 | } 21 | // Toggle to display the custom company form 22 | customBtnEl.onclick = function(){ 23 | customEl.classList.toggle("dn"); 24 | } 25 | // monitor custom inputs 26 | self.customInputs = customEl.querySelectorAll('input'); 27 | for (var i = 0; i < self.customInputs.length; i++) { 28 | self.customInputs[i].oninput = function(){ 29 | self.customCompanyValidate(); 30 | } 31 | } 32 | 33 | // Back button 34 | self.stages["company"].backEl.onclick = function(){ 35 | if(self.stages["industry"].enabled){ 36 | self.showStage("industry"); 37 | } 38 | } 39 | // If a company has been previously selected, ensure that the next button is active, otherwise disable it. 40 | if(self.request.hasOwnProperty("company")){ 41 | self.stages["company"].nextEl.classList.remove("btn-disabled"); 42 | self.stages["company"].nextEl.classList.add("btn-primary"); 43 | } 44 | else{ 45 | self.stages["company"].nextEl.classList.remove("btn-primary"); 46 | self.stages["company"].nextEl.classList.add("btn-disabled"); 47 | } 48 | // If the next button is active, you can use it to move to the next stage. 49 | self.stages["company"].nextEl.onclick = function(){ 50 | var validation_results = self.customCompanyValidate(self.customInputs); 51 | if(validation_results !== false){ 52 | self.selectCustomCompany(customEl, validation_results); 53 | } 54 | else if(self.stages["information"].enabled){ 55 | self.showStage("information"); 56 | } 57 | } 58 | } 59 | 60 | amiApp.selectCompany = function(company_id){ 61 | var self = this; 62 | for (var i = 0; i < self.request.industry.companies.length; i++) { 63 | // Reconcile the company ID with the full company object that's part of the AMI request data structure, and assign that company object to the request. 64 | if(self.request.industry.companies[i].id === company_id){ 65 | self.request.company = self.request.industry.companies[i]; 66 | // Enable the next stage and move the user to it. 67 | self.enableStage("information"); 68 | self.showStage("information"); 69 | self.clearCustomInputs(); 70 | return self.request.company; 71 | } 72 | } 73 | 74 | new Error("Company not found"); 75 | } 76 | 77 | amiApp.selectCustomCompany = function(customEl, validation_results){ 78 | var self = this; 79 | 80 | var company = { 81 | "name": customEl.querySelector('input[name="custom_name"]').value, 82 | "id": "custom", 83 | "logo": null, 84 | "contact": { 85 | "title": customEl.querySelector('input[name="custom_title"]').value, 86 | "has_mail": validation_results.has_mail, 87 | "has_email": validation_results.has_email, 88 | "address1": customEl.querySelector('input[name="custom_address_1"]').value, 89 | "address2": customEl.querySelector('input[name="custom_address_2"]').value, 90 | "city": customEl.querySelector('input[name="custom_city"]').value, 91 | "region": customEl.querySelector('input[name="custom_province"]').value, 92 | "postalcode": customEl.querySelector('input[name="custom_postal_code"]').value, 93 | "country": customEl.querySelector('input[name="custom_country"]').value, 94 | "email": customEl.querySelector('input[name="custom_email"]').value 95 | }, 96 | "industry": "telco" 97 | } 98 | 99 | self.request.company = company; 100 | // Enable the next stage and move the user to it. 101 | self.enableStage("information"); 102 | self.showStage("information"); 103 | 104 | } 105 | 106 | amiApp.clearCustomInputs = function(){ 107 | var self = this; 108 | for (var i = 0; i < self.customInputs.length; i++) { 109 | self.customInputs[i].value = ""; 110 | } 111 | } 112 | 113 | amiApp.customCompanyValidate = function(){ 114 | var self = this; 115 | var completedInputs = 0; 116 | var completedInputsNeeded = 0; 117 | var has_email = false; 118 | var has_mail = false; 119 | 120 | for (var i = 0; i < self.customInputs.length; i++) { 121 | name = self.customInputs[i].getAttribute("name"); 122 | if(name !== "custom_address_2" && name !== "custom_country" && name !== "custom_email"){ 123 | completedInputsNeeded++; 124 | if(self.customInputs[i].value && self.customInputs[i].value !== ""){ 125 | completedInputs++; 126 | } 127 | } 128 | if(name == "custom_email" && self.customInputs[i].value && self.customInputs[i].value !== ""){ 129 | has_email = true; 130 | } 131 | } 132 | 133 | if(completedInputs >= completedInputsNeeded || has_email){ 134 | if(completedInputs >= completedInputsNeeded){ 135 | has_mail = true; 136 | } 137 | self.stages["company"].nextEl.classList.remove("btn-disabled"); 138 | self.stages["company"].nextEl.classList.add("btn-primary"); 139 | return { 140 | has_mail: has_mail, 141 | has_email: has_email 142 | }; 143 | } 144 | else{ 145 | return false; 146 | } 147 | } -------------------------------------------------------------------------------- /static/js/ami_app/crypto.js: -------------------------------------------------------------------------------- 1 | window.crypto = window.crypto || window.msCrypto; 2 | amiApp.enc = new TextEncoder("utf-8"); 3 | amiApp.ami_jwk_key = null; 4 | 5 | // Generate private key for HMAC message signing 6 | // output as JWK format so it can be persisted in localStorage 7 | amiApp.generate_ami_key = function(){ 8 | return window.crypto.subtle.generateKey( 9 | { 10 | name: "HMAC", 11 | hash: {name: "SHA-256"} 12 | }, 13 | true, 14 | ["sign", "verify"] 15 | ) 16 | .then(function(key){ 17 | return crypto.subtle.exportKey("jwk", key) 18 | }); 19 | } 20 | 21 | // import the private key from JWK format into the full format 22 | amiApp.import_ami_key = function(ami_jwk_key){ 23 | return window.crypto.subtle.importKey( 24 | 'jwk', 25 | amiApp.ami_jwk_key, 26 | { 27 | name: "HMAC", 28 | hash: {name: "SHA-256"} 29 | }, 30 | true, 31 | ["sign", "verify"] 32 | ); 33 | } 34 | 35 | // retrieve the key from localstorage 36 | amiApp.retrieve_ami_key = function(){ 37 | var ami_jwk_key = localStorage.getItem("ami_key"); 38 | return JSON.parse(ami_jwk_key); 39 | } 40 | 41 | // Generate or retreive the key if it exists 42 | amiApp.ami_key_promise = new Promise(function(resolve, reject) { 43 | // Try to get the key 44 | amiApp.ami_jwk_key = amiApp.retrieve_ami_key(); 45 | // if no key we have to generate it 46 | if(amiApp.ami_jwk_key === null || Object.keys(amiApp.ami_jwk_key).length === 0){ 47 | // generate key 48 | amiApp.generate_ami_key().then(function(jwtKey){ 49 | // save key to localStorage for future use, and return key 50 | str_key = JSON.stringify(jwtKey); 51 | amiApp.ami_jwk_key = jwtKey; 52 | localStorage.setItem("ami_key", str_key); 53 | resolve(amiApp.ami_jwk_key); 54 | }); 55 | } 56 | else{ 57 | // if key already existed in localstorage, return key 58 | resolve(amiApp.ami_jwk_key) 59 | } 60 | }).then(function(ami_jwk_key){ 61 | // return non jwk key 62 | return amiApp.import_ami_key(ami_jwk_key) 63 | }); 64 | 65 | // convert buffer to hex 66 | amiApp.buf2hex = function(buf) { 67 | var fu = function(x){return ('00'+x.toString(16)).slice(-2)}; 68 | return Array.prototype.map.call(new Uint8Array(buf), fu).join(''); 69 | } 70 | 71 | // generate HMAC from key and message 72 | amiApp.hmacSha256= function(key, str) { 73 | var buf = amiApp.enc.encode(str); 74 | window.theKey = key; 75 | return window.crypto.subtle.sign({name: "HMAC", hash: "SHA-256"}, key, buf).then(function(sig){ 76 | return amiApp.buf2hex(sig); 77 | }) 78 | } -------------------------------------------------------------------------------- /static/js/ami_app/data_utilities.js: -------------------------------------------------------------------------------- 1 | // These functions convert the data in the data.js file into the AMI data structure for use by the app. 2 | amiApp.getIndustry = function(industry_id){ 3 | var industry = null; 4 | for (var i = 0; i < amiApp.dataSource.industries.length; i++) { 5 | if(amiApp.dataSource.industries[i].id === industry_id){ 6 | industry = amiApp.dataSource.industries[i]; 7 | return industry; 8 | } 9 | else{ 10 | new Error("Industry with id " + industry_id + " not found."); 11 | } 12 | } 13 | } 14 | amiApp.getIndustryCompanies = function(industry){ 15 | var companies = []; 16 | for (var i = 0; i < amiApp.dataSource.companies.length; i++) { 17 | if(amiApp.dataSource.companies[i].industry == industry.id){ 18 | companies.push(amiApp.dataSource.companies[i]); 19 | } 20 | } 21 | return companies; 22 | } 23 | amiApp.getIndustryInfoCategories = function(industry){ 24 | var info_categories = []; 25 | for (var i = 0; i < amiApp.dataSource.info_categories.length; i++) { 26 | for (var j = 0; j < amiApp.dataSource.info_categories[i].industries.length; j++) { 27 | if(amiApp.dataSource.info_categories[i].industries[j] == industry.id){ 28 | info_categories.push(amiApp.dataSource.info_categories[i]); 29 | } 30 | } 31 | } 32 | return info_categories; 33 | } 34 | amiApp.getIndustryPersonalIdentifiers = function(industry){ 35 | var personal_identifiers = []; 36 | for (var i = 0; i < amiApp.dataSource.personal_identifiers.length; i++) { 37 | for (var j = 0; j < amiApp.dataSource.personal_identifiers[i].industries.length; j++) { 38 | if(amiApp.dataSource.personal_identifiers[i].industries[j] == industry.id){ 39 | personal_identifiers.push(amiApp.dataSource.personal_identifiers[i]); 40 | } 41 | } 42 | } 43 | return personal_identifiers; 44 | } 45 | amiApp.getData = function(url, callback){ 46 | var xmlhttp = new XMLHttpRequest(); 47 | xmlhttp.onreadystatechange = function() { 48 | if (this.readyState == 4 && this.status == 200) { 49 | var myArr = JSON.parse(this.responseText); 50 | callback(myArr); 51 | } 52 | }; 53 | xmlhttp.open("GET", url, true); 54 | xmlhttp.send(); 55 | } -------------------------------------------------------------------------------- /static/js/ami_app/identifiers.js: -------------------------------------------------------------------------------- 1 | amiApp.personal_identifiers_form_controller = function(targetEl, buttonTargetEl, companyNameTargetEl){ 2 | var self = this; 3 | self.activeStage = "identifiers"; 4 | location.hash = "#id"; 5 | 6 | // RESET TEMPLATE 7 | targetEl.innerHTML = ""; 8 | buttonTargetEl.innerHTML = ""; 9 | 10 | // Create next button 11 | var nextButtonEl = self.createFromTemplate("personal_identifiers_button_template", null) 12 | self.personal_identifiers_nextButtonEl = nextButtonEl; 13 | 14 | // build out the form containing inputs for the required personal identifiers used in the request letter. 15 | for (var i = 0; i < self.request.industry.personal_identifiers.length; i++) { 16 | el = self.createFromTemplate("personal_identifiers_template", self.request.industry.personal_identifiers[i]); 17 | // Check the personal identifer object and see if there are options associated with it. If there are not, we assume a regular text input. Otherwise we create a select element with options. 18 | if(!Array.isArray(self.request.industry.personal_identifiers[i].options)){ 19 | el.querySelector("input").name = self.request.industry. personal_identifiers[i].id 20 | el.querySelector("input").setAttribute('desc', self.request.industry.personal_identifiers[i].name); 21 | } 22 | else{ 23 | // Create select element 24 | newel = document.createElement("select"); 25 | newel.classList.add("db"); 26 | newel.setAttribute("name", self.request.industry.personal_identifiers[i].id); 27 | newel.setAttribute('desc', self.request.industry.personal_identifiers[i].name); 28 | 29 | // Loop through options associated with the identifer and add them to the select element. 30 | for (var j = 0; j < self.request.industry.personal_identifiers[i].options.length; j++) { 31 | option = document.createElement("option") 32 | option.value = self.request.industry.personal_identifiers[i].options[j].id; 33 | option.text = self.request.industry.personal_identifiers[i].options[j].name; 34 | newel.appendChild(option); 35 | } 36 | 37 | // Replace input (the element based on the template) with this select element. This is a hacky override. 38 | el.querySelector("input").outerHTML = newel.outerHTML; 39 | } 40 | 41 | // Add values to the inputs if the user has already completed this stage. Keep in mind we are still in a huge for loop 42 | if(typeof self.request['personal_identifiers'] !== "undefined"){ 43 | // Check if there are identifiers associated with the request object 44 | for (var j = 0; j < self.request['personal_identifiers'].length; j++) { 45 | // Match identifier by ID 46 | if(self.request['personal_identifiers'][j].id === self.request.industry.personal_identifiers[i].id){ 47 | if(!Array.isArray(self.request.industry.personal_identifiers[i].options)){ 48 | // add value to input if there's a match 49 | el.querySelector("input").value = self.request['personal_identifiers'][j].value; 50 | } 51 | else{ 52 | // select option if there's a match 53 | el.querySelector("select").value = self.request['personal_identifiers'][j].value; 54 | } 55 | } 56 | } 57 | } 58 | // Any time an input changes value, re-validate the form 59 | el.oninput = function(){ 60 | self.personal_identifiers_validate(nextButtonEl); 61 | } 62 | targetEl.appendChild(el); 63 | } 64 | 65 | // Put the company name into the template element 66 | company_name_el = self.createFromTemplate("identifiers_company_template", self.request.company); 67 | companyNameTargetEl.innerHTML = ""; 68 | companyNameTargetEl.appendChild(company_name_el); 69 | 70 | // Assign the genereated input and select elements to a variable 71 | // for use in the validation functions 72 | self.personal_identifiers_inputs = targetEl.querySelectorAll("input, select"); 73 | 74 | // Validate identifiers immediately once the stage is active, 75 | // since the stage could be accessed by hitting the back button 76 | self.personal_identifiers_validate(nextButtonEl); 77 | 78 | // Once the next button is clicked, validate the inputs. 79 | nextButtonEl.onclick = function(){ 80 | completed_identifiers = self.personal_identifiers_validate(nextButtonEl); 81 | // If at least one identifier passes validation, we can check for the stats opt out and move on 82 | if(completed_identifiers.length > 0){ 83 | if(typeof amiApp.stats !== "undefined" && typeof amiApp.stats.token !== "undefined"){ 84 | self.assignStatsOptOut(); 85 | } 86 | self.assignPersonalIdentifiers(completed_identifiers); 87 | } 88 | } 89 | // Set up back button functionality 90 | self.stages["identifiers"].backEl.onclick = function(){ 91 | if(self.stages["information"].enabled){ 92 | self.showStage("information"); 93 | } 94 | } 95 | buttonTargetEl.appendChild(nextButtonEl); 96 | 97 | if(typeof amiApp.stats !== "undefined" && typeof amiApp.stats.token !== "undefined"){ 98 | // set up opt-out checkbox to send stats 99 | var optout_container = document.getElementById("optout_container"); 100 | var optout = document.getElementById("optout"); 101 | optout_container.classList.remove("dn"); 102 | if(amiApp.stats.optOut){ 103 | optout.checked = false; 104 | } 105 | else{ 106 | optout.checked = true; 107 | } 108 | } 109 | 110 | // If a user clicks on the top nav element for the next stage, make sure to clean up this stage (i.e. interpret it as a next button click essentially) 111 | self.stages["request"].navEl.addEventListener('click', amiApp.resolveIdentifierStage, true); 112 | } 113 | 114 | // This function checks to see if there's a value in each identifier input 115 | // field and if so, push the value and the id of that identifeir to an array 116 | // If the array has at least some items in it, we enable the next stage. 117 | amiApp.personal_identifiers_validate = function(nextButtonEl){ 118 | var self = this; 119 | var completedInputsCount = 0; 120 | var completed_identifiers = []; 121 | 122 | var inputs = self.personal_identifiers_inputs; 123 | 124 | // Check input values 125 | for (var i = 0; i < inputs.length; i++) { 126 | if(inputs[i].value !== ""){ 127 | completedInputsCount++; 128 | } 129 | // Push completed input values to array 130 | if(inputs[i].value){ 131 | completed_identifiers.push({ 132 | id: inputs[i].getAttribute('name'), 133 | name: inputs[i].getAttribute("desc"), 134 | value: inputs[i].value, 135 | }); 136 | } 137 | } 138 | // Enable / disable next stage based on completion of the inputs 139 | if(completedInputsCount > 0){ 140 | self.enableStage("request"); 141 | } 142 | else{ 143 | self.disableStage("request"); 144 | } 145 | // Enable / disable next button based on completion of the inputs 146 | if(completedInputsCount > 0){ 147 | nextButtonEl.classList.remove("btn-disabled"); 148 | nextButtonEl.classList.add("btn-primary"); 149 | } 150 | else{ 151 | nextButtonEl.classList.remove("btn-primary"); 152 | nextButtonEl.classList.add("btn-disabled"); 153 | } 154 | return completed_identifiers; 155 | } 156 | 157 | // Assign completed personal identifers to master request object. 158 | amiApp.assignPersonalIdentifiers = function(completed_identifiers){ 159 | var self = this; 160 | self.request.personal_identifiers = []; 161 | self.request.personal_identifiers = completed_identifiers; 162 | self.showStage("request"); 163 | } 164 | 165 | amiApp.resolveIdentifierStage = function(){ 166 | var self = amiApp; 167 | // validate identifers 168 | var completed_identifiers = self.personal_identifiers_validate(self.info_categories_nextButtonEl); 169 | if(completed_identifiers.length > 0){ 170 | // assign identifers to request object 171 | if(typeof amiApp.stats !== "undefined" && typeof amiApp.stats.token !== "undefined"){ 172 | self.assignStatsOptOut(); 173 | } 174 | self.assignPersonalIdentifiers(completed_identifiers); 175 | } 176 | // "this" here is the top level nav entry for the next stage, the request stage. Once we click it once, we don't want to trigger it again (i.e. if the user clicks it from another stage. The functionality need only be active when on this current, identifier, stage) 177 | this.removeEventListener('click', amiApp.resolveIdentifierStage, true); 178 | } 179 | // based on checkbox value, assign opt out status to requester 180 | amiApp.assignStatsOptOut = function(){ 181 | var optout = document.getElementById("optout"); 182 | if(optout.checked){ 183 | amiApp.stats.optOut = false; 184 | } 185 | else{ 186 | amiApp.stats.optOut = true; 187 | } 188 | console.log("OPPT", amiApp.stats.optOut) 189 | } -------------------------------------------------------------------------------- /static/js/ami_app/industry.js: -------------------------------------------------------------------------------- 1 | amiApp.industry_list_controller = function(targetEl){ 2 | var self = this; 3 | self.activeStage = "industry"; 4 | location.hash = "#home"; 5 | 6 | // RESET TEMPLATE 7 | targetEl.innerHTML = ""; 8 | 9 | // Create elements representing each industry 10 | for (var i = 0; i < self.industries.length; i++) { 11 | el = self.createFromTemplate("industry_select_template", self.industries[i]); 12 | // If the user has gone back a stage, make sure to highlight the industry they had selected 13 | if(self.industries[i] === self.request.industry){ 14 | el.classList.add("btn-media-select"); 15 | } 16 | // Clicking an industry element will result in selecting that industry for the request. 17 | el.onclick = function(el){ 18 | var id = this.querySelector('*[ami_template_value_container="id"]').innerHTML; 19 | this.classList.add("btn-media-select"); 20 | self.enableStage("company"); 21 | // assign industry to request object here 22 | self.selectIndustry(id); 23 | } 24 | targetEl.appendChild(el); 25 | } 26 | } 27 | amiApp.selectIndustry = function(industry_id){ 28 | var self = this; 29 | for (var i = 0; i < self.industries.length; i++) { 30 | // Reconcile the industry ID with the full industry object that's part of the AMI data structure, and assign that industry object to the request. 31 | if(self.industries[i].id === industry_id){ 32 | if(self.request.industry && industry_id !== self.request.industry.id){ 33 | // Since the industry has changed, we have to disable later stages because the user must go through the form in order. 34 | self.disableStage("information"); 35 | } 36 | self.request.industry = self.industries[i]; 37 | // delete the selected company, since the industry has changed 38 | delete(self.request.company); 39 | // Move to next stage 40 | self.showStage("company") 41 | return self.request.industry; 42 | } 43 | } 44 | 45 | new Error("Industry not found"); 46 | } -------------------------------------------------------------------------------- /static/js/ami_app/info.js: -------------------------------------------------------------------------------- 1 | amiApp.info_categories_form_controller = function(targetEl, nextButtonTargetEl, companyNameTargetEl, companyNameTargetEl2){ 2 | var self = this; 3 | self.activeStage = "information"; 4 | location.hash = "#info"; 5 | 6 | // RESET TEMPLATE 7 | targetEl.innerHTML = ""; 8 | nextButtonTargetEl.innerHTML = ""; 9 | 10 | // Create next button 11 | var nextButtonEl = self.createFromTemplate("info_category_select_button_template", null) 12 | 13 | // Loop through the information categories associated with the 14 | // selected industry, and create checkboxes for them. 15 | for (var i = 0; i < self.request.industry.info_categories.length; i++) { 16 | // build checkbox element from template 17 | el = self.createFromTemplate("info_category_select_template", self.request.industry.info_categories[i]); 18 | // associate the checkbox with a particular info category id 19 | el.querySelector("input[type='checkbox']").value = self.request.industry.info_categories[i].id; 20 | 21 | // Another level loop, through the request object's info categories 22 | // if they are set. 23 | // This is used to check/uncheck the checkboxes if the user has 24 | // already completed this stage, to preserve their previous inputs 25 | if(typeof self.request['info_categories'] !== "undefined"){ 26 | hasMatch = false; 27 | for (var j = 0; j < self.request['info_categories'].length; j++) { 28 | if(self.request['info_categories'][j].id === self.request.industry.info_categories[i].id){ 29 | el.querySelector("input[type='checkbox']").checked = true; 30 | hasMatch = true; 31 | } 32 | } 33 | if(!hasMatch){ 34 | el.querySelector("input[type='checkbox']").checked = false; 35 | } 36 | } 37 | // If a checkbox is checked or unchecked, we want to re-validate 38 | el.onclick = function(){ 39 | self.info_categories_validate(nextButtonEl); 40 | } 41 | // add checkbox to DOM 42 | targetEl.appendChild(el); 43 | } 44 | 45 | // This is a messy bit of functions that add the company name to a few spots in the overall form. 46 | company_name_el = self.createFromTemplate("info_company_template", self.request.company); 47 | companyNameTargetEl.innerHTML = ""; 48 | companyNameTargetEl.appendChild(company_name_el); 49 | company_name_el2 = self.createFromTemplate("info_company_template", self.request.company); 50 | companyNameTargetEl2.innerHTML = ""; 51 | companyNameTargetEl2.appendChild(company_name_el2); 52 | 53 | // Assign the checkbox elements to a variable we can re use in different contexts (i.e. validations) 54 | self.info_categories_checkboxes = targetEl.querySelectorAll("input[type='checkbox']") 55 | 56 | // Wire up the next button to validate checkboxes, and if there are some checked items, assign them to the request 57 | nextButtonEl.onclick = function(){ 58 | var selected_info_categories = self.info_categories_validate(self.info_categories_nextButtonEl); 59 | if(selected_info_categories.length > 0){ 60 | // assign selected info categories to the request 61 | self.selectInfoCategories(selected_info_categories); 62 | } 63 | } 64 | 65 | // Wire up back button to go back one stage 66 | self.stages["information"].backEl.onclick = function(){ 67 | if(self.stages["company"].enabled){ 68 | self.showStage("company"); 69 | } 70 | } 71 | 72 | // validate the checkboxes upon stage load in case of previous input. 73 | self.info_categories_validate(nextButtonEl); 74 | 75 | self.info_categories_nextButtonEl = nextButtonEl; 76 | nextButtonTargetEl.appendChild(nextButtonEl); 77 | 78 | // If a user clicks on the top nav element for the next stage, make sure to clean up this stage (i.e. interpret it as a next button click essentially) 79 | self.stages["identifiers"].navEl.addEventListener('click', amiApp.assignInfoCategories, true); 80 | } 81 | 82 | // This function checks to see if at least one checkbox is checked. 83 | // If so, push the value of the checked info category to an array 84 | // If the array has at least some items in it, we enable the next stage. 85 | amiApp.info_categories_validate = function(nextButtonEl){ 86 | var self = this; 87 | var selectedInfosCount = 0; 88 | var selected_info_categories = []; 89 | 90 | for (var i = 0; i < self.info_categories_checkboxes.length; i++) { 91 | // determine if it's checked 92 | if(self.info_categories_checkboxes[i].checked){ 93 | selectedInfosCount++; 94 | // assign checked info category to array 95 | selected_info_categories.push(self.info_categories_checkboxes[i].value); 96 | } 97 | } 98 | // Enable / disable next stage based on completion of the checkboxes 99 | if(selectedInfosCount){ 100 | self.enableStage("identifiers"); 101 | } 102 | else{ 103 | self.disableStage("identifiers"); 104 | } 105 | // Enable / disable next button based on completion of the inputs 106 | if(selectedInfosCount){ 107 | nextButtonEl.classList.remove("btn-disabled"); 108 | nextButtonEl.classList.add("btn-primary"); 109 | } 110 | else{ 111 | nextButtonEl.classList.remove("btn-primary"); 112 | nextButtonEl.classList.add("btn-disabled"); 113 | } 114 | return selected_info_categories; 115 | } 116 | 117 | // Asssign completed info categories to master request object. 118 | amiApp.assignInfoCategories = function(){ 119 | var self = amiApp; 120 | var selected_info_categories = self.info_categories_validate(self.info_categories_nextButtonEl); 121 | if(selected_info_categories.length > 0){ 122 | // assign selected info categires to request object 123 | self.selectInfoCategories(selected_info_categories); 124 | } 125 | // "this" here is the top level nav entry for the next stage, the identifiers stage. Once we click it once, we don't want to trigger it again (i.e. if the user clicks it from another stage. The functionality need only be active when on this current, info, stage) 126 | this.removeEventListener('click', amiApp.assignInfoCategories, true); 127 | } 128 | 129 | // Assign selected info categories to master request object. 130 | amiApp.selectInfoCategories = function(selected_info_categories){ 131 | var self = this; 132 | self.request.info_categories = []; 133 | for (var i = 0; i < self.request.industry.info_categories.length; i++) { 134 | info_cat = self.request.industry.info_categories[i]; 135 | for (var j = 0; j < selected_info_categories.length; j++) { 136 | // reconcile checked input id with info category object, and assign the object to the request, not just the ID. 137 | if (selected_info_categories[j] == info_cat.id) { 138 | self.request.info_categories.push(info_cat); 139 | } 140 | } 141 | } 142 | // Move to next stage. 143 | self.enableStage("identifiers"); 144 | self.showStage("identifiers"); 145 | } 146 | -------------------------------------------------------------------------------- /static/js/ami_app/init.js: -------------------------------------------------------------------------------- 1 | window.onbeforeunload = function () { 2 | window.scrollTo(0, 0); 3 | } 4 | if(document.location.protocol === 'http:'){ 5 | document.getElementById('securityWarning').classList.remove("dn"); 6 | } 7 | 8 | var amiApp = {} 9 | amiApp.industries = [] 10 | amiApp.request = {} 11 | 12 | amiApp.initialize = function(){ 13 | var self = this; 14 | self.templates = {} 15 | self.getTemplate("industry_select_template"); 16 | self.getTemplate("company_select_template"); 17 | self.getTemplate("info_category_select_template"); 18 | self.getTemplate("info_category_select_button_template") 19 | self.getTemplate("personal_identifiers_template"); 20 | self.getTemplate("personal_identifiers_button_template") 21 | self.getTemplate("request_date_template") 22 | self.getTemplate("request_industry_name_template") 23 | self.getTemplate("request_company_contact_template") 24 | self.getTemplate("request_information_category_template") 25 | self.getTemplate("request_personal_identifier_template") 26 | self.getTemplate("request_signature_template") 27 | self.getTemplate("request_email_button_template") 28 | self.getTemplate("request_gmail_button_template") 29 | self.getTemplate("request_pdf_button_template") 30 | self.getTemplate("request_company_address_template") 31 | self.getTemplate("request_company_email_template") 32 | self.getTemplate("info_company_template") 33 | self.getTemplate("identifiers_company_template") 34 | self.getTemplate("request_company_template") 35 | 36 | self.industries = self.dataSource.industries; 37 | for (var i = 0; i < self.industries.length; i++) { 38 | self.industries[i].companies = self.getIndustryCompanies(self.industries[i]) 39 | self.industries[i].info_categories = self.getIndustryInfoCategories(self.industries[i]); 40 | self.industries[i].personal_identifiers = self.getIndustryPersonalIdentifiers(self.industries[i]); 41 | } 42 | self.stages = { 43 | "industry": { 44 | enabled: false, 45 | navEl: document.getElementById('nav_el_industry'), 46 | }, 47 | "company": { 48 | enabled: false, 49 | navEl: document.getElementById('nav_el_company'), 50 | backEl: document.getElementById('back_el_company'), 51 | nextEl: document.getElementById('next_el_company') 52 | }, 53 | "information": { 54 | enabled: false, 55 | navEl: document.getElementById('nav_el_information'), 56 | backEl: document.getElementById('back_el_information'), 57 | nextEl: document.getElementById('next_el_information') 58 | }, 59 | "identifiers": { 60 | enabled: false, 61 | navEl: document.getElementById('nav_el_identifiers'), 62 | backEl: document.getElementById('back_el_identifiers'), 63 | nextEl: document.getElementById('next_el_identifiers') 64 | }, 65 | "request": { 66 | enabled: false, 67 | navEl: document.getElementById('nav_el_request'), 68 | backEl: document.getElementById('back_el_request') 69 | } 70 | } 71 | self.stageListController(); 72 | self.disableStage("industry"); 73 | self.enableStage("industry"); 74 | self.showStage("industry"); 75 | } -------------------------------------------------------------------------------- /static/js/ami_app/request.js: -------------------------------------------------------------------------------- 1 | amiApp.requestLetterController = function( 2 | requestTargetEls, 3 | requestEl, 4 | pdfButtonTargetEl, 5 | emailButtonTargetEl, 6 | method_title_bothEl, 7 | method_title_mail_onlyEl, 8 | method_title_email_onlyEl, 9 | mail_methodEl, 10 | email_methodEl, 11 | request_copy_button, 12 | gmailButtonTargetEl 13 | ){ 14 | var data; 15 | var self = this; 16 | self.activeStage = "request"; 17 | location.hash = "#requestLetter"; 18 | 19 | // The request stage is much more complicated than the others, due to 20 | // the number of different components. For this reason, we loop 21 | // through target elements for each of those components, and then 22 | // in the "switch" statement, match them to templates and create 23 | // the templates based on the appropirate request data. 24 | for (var property in requestTargetEls) { 25 | if (requestTargetEls.hasOwnProperty(property)) { 26 | switch(property){ 27 | case "company_name": 28 | // add company name to stage heading 29 | el = self.createFromTemplate("request_company_template", self.request.company); 30 | requestTargetEls[property].innerHTML = ""; 31 | requestTargetEls[property].appendChild(el); 32 | break; 33 | case "date": 34 | // add date to request letter 35 | el = self.createFromTemplate("request_date_template", { 36 | date: new Date().toDateString(), 37 | }); 38 | requestTargetEls[property].innerHTML = ""; 39 | requestTargetEls[property].appendChild(el); 40 | break; 41 | case "contact": 42 | // Add the company contact information to the request letter 43 | el = self.createFromTemplate("request_company_contact_template", self.request.company.contact); 44 | requestTargetEls[property].innerHTML = ""; 45 | requestTargetEls[property].appendChild(el); 46 | break; 47 | case "info_categories": 48 | // Add the selected info categories to the request letter 49 | requestTargetEls[property].innerHTML = ""; 50 | for (var i = 0; i < self.request.info_categories.length; i++) { 51 | el = self.createFromTemplate("request_information_category_template", self.request.info_categories[i]); 52 | requestTargetEls[property].appendChild(el); 53 | } 54 | break; 55 | case "personal_identifiers": 56 | // Add the completed personal identifiers to the request letter 57 | requestTargetEls[property].innerHTML = ""; 58 | for (var i = 0; i < self.request.personal_identifiers.length; i++) { 59 | console.log(self.request.personal_identifiers[i]) 60 | el = self.createFromTemplate("request_personal_identifier_template", self.request.personal_identifiers[i]); 61 | requestTargetEls[property].appendChild(el); 62 | } 63 | break; 64 | case "signature": 65 | requestTargetEls[property].innerHTML = ""; 66 | // Add the first name and last name from the completed personal identifiers (if available) to the request letter 67 | signature_components = ["firstname", "lastname"]; 68 | signature_data = {}; 69 | for (var i = 0; i < self.request.personal_identifiers.length; i++) { 70 | if(signature_components.indexOf(self.request.personal_identifiers[i].id) >= 0){ 71 | signature_data[self.request.personal_identifiers[i].id] = self.request.personal_identifiers[i].value; 72 | } 73 | } 74 | el = self.createFromTemplate("request_signature_template", signature_data); 75 | requestTargetEls[property].appendChild(el); 76 | break; 77 | case "address": 78 | // Create instructinon area for sending request letter 79 | // via postal mail 80 | el = self.createFromTemplate("request_company_address_template", self.request.company.contact); 81 | requestTargetEls[property].innerHTML = ""; 82 | requestTargetEls[property].appendChild(el); 83 | break; 84 | case "email": 85 | // Create instructinon area for sending request letter 86 | // via email 87 | el = self.createFromTemplate("request_company_email_template", self.request.company.contact); 88 | requestTargetEls[property].innerHTML = ""; 89 | requestTargetEls[property].appendChild(el); 90 | break; 91 | } 92 | } 93 | 94 | // Display letter generating tools based on company contact method booleans 95 | if(self.request.company.contact.has_email && self.request.company.contact.has_mail){ 96 | // Display both postal and email options 97 | method_title_bothEl.classList.remove('dn'); 98 | method_title_mail_onlyEl.classList.add('dn'); 99 | method_title_email_onlyEl.classList.add('dn'); 100 | mail_methodEl.classList.remove("dn"); 101 | email_methodEl.classList.remove("dn"); 102 | } 103 | else if (self.request.company.contact.has_email){ 104 | // display only email option 105 | method_title_bothEl.classList.add('dn'); 106 | method_title_mail_onlyEl.classList.add('dn'); 107 | method_title_email_onlyEl.classList.remove('dn'); 108 | mail_methodEl.classList.add("dn"); 109 | email_methodEl.classList.remove("dn"); 110 | } 111 | else if (self.request.company.contact.has_mail){ 112 | // display only postal mail option 113 | method_title_bothEl.classList.add('dn'); 114 | method_title_mail_onlyEl.classList.remove('dn'); 115 | method_title_email_onlyEl.classList.add('dn'); 116 | mail_methodEl.classList.remove("dn"); 117 | email_methodEl.classList.add("dn"); 118 | } 119 | else { 120 | method_title_bothEl.classList.add('dn'); 121 | method_title_mail_onlyEl.classList.add('dn'); 122 | method_title_email_onlyEl.classList.add('dn'); 123 | mail_methodEl.classList.add("dn"); 124 | email_methodEl.classList.add("dn"); 125 | } 126 | } 127 | 128 | // Create the PDF Generating button 129 | pdfButtonTargetEl.innerHTML = ""; 130 | var pdfButtonEl = self.createFromTemplate("request_pdf_button_template", null) 131 | // Wire up the button so it creates a PDF when clicked 132 | pdfButtonEl.onclick = function(){ 133 | amiApp.makePDF(requestEl, "letter"); 134 | } 135 | // Add button to the DOM 136 | pdfButtonTargetEl.appendChild(pdfButtonEl); 137 | 138 | // Create email and gmail buttons 139 | emailButtonTargetEl.innerHTML = ""; 140 | gmailButtonTargetEl.innerHTML = ""; 141 | var emailButtonEl = self.createFromTemplate("request_email_button_template", null) 142 | var gmailButtonEl = self.createFromTemplate("request_gmail_button_template", null) 143 | 144 | // Add email links to each button 145 | emailButtonEl.setAttribute("href", amiApp.buildEmail(amiApp.dataSource.request_subject_line, requestEl, "mailto")); 146 | gmailButtonEl.setAttribute("href", amiApp.buildEmail(amiApp.dataSource.request_subject_line, requestEl, "gmail")); 147 | 148 | // add email and gmail buttons to DOM 149 | emailButtonTargetEl.appendChild(emailButtonEl); 150 | gmailButtonTargetEl.appendChild(gmailButtonEl); 151 | 152 | // Wire up the back button 153 | self.stages["request"].backEl.onclick = function(){ 154 | if(self.stages["identifiers"].enabled){ 155 | self.showStage("identifiers"); 156 | } 157 | } 158 | 159 | // wire up the button that lets you copy the request's text. 160 | request_copy_button.onclick = function(){ 161 | var lastChar = request_copy_button.innerHTML[request_copy_button.innerHTML.length -1] 162 | if(lastChar !== "✓"){ 163 | request_copy_button.innerHTML += " ✓"; 164 | } 165 | copyText(document.getElementById("request")); 166 | } 167 | 168 | // If the statistics functioality is enabled, and if the user has not opted-out, generate an HMAC of the request and send it to the server. 169 | if(typeof amiApp.stats !== "undefined" && typeof amiApp.stats.token !== "undefined" && !amiApp.stats.optOut){ 170 | console.log("ass") 171 | // Wait until the crypto key is available 172 | amiApp.ami_key_promise.then(function(ami_key){ 173 | var msg = ""; 174 | msg = msg + amiApp.request.company.name; 175 | 176 | // creat the HMAC based on the company name and the private key 177 | amiApp.hmacSha256(ami_key, msg).then(function(hmac_sig){ 178 | // populate the hidden stats form with the HMAC value, the CSRF token, and the company id. 179 | var companyInput = document.getElementById("request_form_company"); 180 | var hmacInput = document.getElementById("request_form_hmac"); 181 | var tokenInput = document.getElementById("request_form_token"); 182 | 183 | companyInput.setAttribute('value', amiApp.request.company.id); 184 | hmacInput.setAttribute('value', hmac_sig); 185 | tokenInput.setAttribute('value', amiApp.stats.token); 186 | 187 | // submit the form (function is in the stats_helpers.js file) 188 | makePostRequest(amiApp.stats.postURL); 189 | }); 190 | }); 191 | } 192 | } -------------------------------------------------------------------------------- /static/js/ami_app/request_helpers.js: -------------------------------------------------------------------------------- 1 | amiApp.makePDF = function(element, papersize){ 2 | var requestLetter = new CanvasDocument(papersize, [11.7647, 11.7647, 11.7647, 11.7647]); 3 | 4 | // convert HTML in #request element to canvas-based document 5 | requestLetter.writeHTMLtoDoc(element); 6 | 7 | // convert series of canvases into PDF 8 | requestLetter.createPDF(); 9 | requestLetter.savePDF(amiApp.dataSource.request_pdf_filename); 10 | } 11 | 12 | amiApp.buildEmail = function(subject, el, option){ 13 | var self = this; 14 | var to, subject, body, email, el, clone, listItems; 15 | to = self.request.company.contact.email; 16 | 17 | listItems = el.getElementsByTagName("li"); 18 | var listIndex = 0; 19 | for (var i = 0; i < listItems.length; i++) { 20 | var listSymbol = "* "; 21 | var newList = true; 22 | if(i > 0 && listItems[i-1].parentNode !== listItems[i].parentNode){ 23 | var newList = true; 24 | } 25 | else{ 26 | newList = false; 27 | } 28 | if(newList){ 29 | listIndex = 0; 30 | } 31 | if(listItems[i].parentNode.tagName == "OL"){ 32 | if(listItems[i].parentNode.getAttribute("type") == "A"){ 33 | listSymbol = String.fromCharCode(97 + listIndex).toUpperCase()+". "; 34 | } 35 | else{ 36 | listSymbol = listIndex+1+". "; 37 | } 38 | } 39 | listItems[i].innerHTML = listSymbol + listItems[i].innerHTML + "
"; 40 | listIndex++; 41 | }; 42 | 43 | body = getInnerText(el).replace(/^\s+|\s+$/g, '').replace(/\n,'\r\n'/); 44 | 45 | for (var i = 0; i < listItems.length; i++) { 46 | listItems[i].innerHTML = listItems[i].innerHTML.substring(2); 47 | }; 48 | // console.log("body", body, el); 49 | if(option == "gmail"){ 50 | email = "https://mail.google.com/mail/?view=cm&fs=1&to=" + to + "&su=" + encodeURIComponent(subject); 51 | } 52 | else{ 53 | email = "mailto:" + to + "?subject=" + encodeURIComponent(subject); 54 | } 55 | return email; 56 | } 57 | 58 | var getInnerText = function(el) { 59 | var sel, range, innerText = ""; 60 | if (typeof document.selection != "undefined" && typeof document.body.createTextRange != "undefined") { 61 | range = document.body.createTextRange(); 62 | range.moveToElementText(el); 63 | innerText = range.text; 64 | } else if (typeof window.getSelection != "undefined" && typeof document.createRange != "undefined") { 65 | sel = window.getSelection(); 66 | sel.selectAllChildren(el); 67 | innerText = "" + sel; 68 | sel.removeAllRanges(); 69 | } 70 | return innerText; 71 | } 72 | function copyText(node) { 73 | if (document.body.createTextRange) { 74 | const range = document.body.createTextRange(); 75 | range.moveToElementText(node); 76 | range.select(); 77 | document.execCommand("copy"); 78 | } else if (window.getSelection) { 79 | const selection = window.getSelection(); 80 | const range = document.createRange(); 81 | range.selectNodeContents(node); 82 | selection.removeAllRanges(); 83 | selection.addRange(range); 84 | document.execCommand("copy"); 85 | selection.removeAllRanges(); 86 | } else { 87 | console.warn("Could not select text in node: Unsupported browser."); 88 | } 89 | } -------------------------------------------------------------------------------- /static/js/ami_app/router.js: -------------------------------------------------------------------------------- 1 | // the router watches the location.hash property and shows the appropriate stage. 2 | // the showStage function and most of the stage logic is in stages.js 3 | 4 | amiApp.router = {} 5 | 6 | amiApp.router.handle = function(){ 7 | var self = amiApp; 8 | console.log("hi"); 9 | switch (location.hash) { 10 | case "#home": 11 | case "#": 12 | case "": 13 | if (self.activeStage !== "industry"){ 14 | self.showStage("industry"); 15 | } 16 | break; 17 | case "#org": 18 | if(self.stages["company"].enabled && self.activeStage !== "company"){ 19 | self.showStage("company"); 20 | } 21 | break; 22 | case "#info": 23 | if(self.stages["information"].enabled && self.activeStage !== "information"){ 24 | self.showStage("information"); 25 | } 26 | break; 27 | case "#id": 28 | if(self.stages["identifiers"].enabled && self.activeStage !== "identifiers"){ 29 | self.showStage("identifiers"); 30 | } 31 | break; 32 | case "#request": 33 | if(self.stages["request"].enabled && self.activeStage !== "request"){ 34 | self.showStage("request"); 35 | } 36 | break; 37 | } 38 | } 39 | 40 | window.onhashchange = amiApp.router.handle; -------------------------------------------------------------------------------- /static/js/ami_app/stages.js: -------------------------------------------------------------------------------- 1 | // Logic that controls the activating/showing of particular stages in the 2 | // AMI form, as well as enabling and disabling them, and controlling the 3 | // top level nav bar. 4 | // stages are referred to throughout the app, and operations taken elsewhere on the various stages trigger changes to stages being enabled/disabled/activated. 5 | 6 | // Enables a given stage, and removes the disabled class from its top nav el 7 | amiApp.enableStage = function(stage){ 8 | var self = this; 9 | self.stages[stage].enabled = true; 10 | self.stages[stage].navEl.classList.remove('disabled'); 11 | } 12 | 13 | // Disables a given stage, and add the disabled class from its top nav el 14 | // Also do the same thing FOR ALL LATER STAGES based on the order in which the stages are defined in init.js' initialize() function, This way if company stage is disabled, info, identifiers, and request stage are also disabled. 15 | amiApp.disableStage = function(stage_to_disable){ 16 | var self = this; 17 | var disableStage = false; 18 | for (var stage in self.stages) { 19 | if (self.stages.hasOwnProperty(stage)) { 20 | if(stage == stage_to_disable){ 21 | console.log('starting to disable', stage); 22 | disableStage = true; 23 | } 24 | if(disableStage){ 25 | console.log(stage) 26 | self.stages[stage].enabled = false; 27 | self.stages[stage].navEl.classList.add("disabled"); 28 | } 29 | } 30 | } 31 | } 32 | 33 | // All the stuff that happens when a new stage is selected and shown (i.e. activated). 34 | amiApp.showStage = function(stage){ 35 | var self = this; 36 | stageEls = { 37 | "industry": document.getElementById('stage_industry'), 38 | "company": document.getElementById('stage_company'), 39 | "information": document.getElementById('stage_information'), 40 | "identifiers": document.getElementById('stage_identifiers'), 41 | "request": document.getElementById('stage_request') 42 | } 43 | // hide all stages 44 | hideOtherStages = function(){ 45 | for (var stage in stageEls) { 46 | if (stageEls.hasOwnProperty(stage)) { 47 | stageEls[stage].classList.add("dn"); 48 | } 49 | } 50 | } 51 | // update the top level stage nav element classes to reflect which stage is active, which are past, and which are future. 52 | updateStageNav = function(activeStage){ 53 | var stageIndex = 0; 54 | for (var stage in self.stages) { 55 | if (self.stages.hasOwnProperty(stage)) { 56 | stageIndex++; 57 | self.stages[stage].stageIndex = stageIndex; 58 | // remove active class from all stage navEls 59 | self.stages[stage].navEl.classList.remove("active"); 60 | } 61 | } 62 | for (var stage in self.stages) { 63 | if (self.stages.hasOwnProperty(stage)) { 64 | 65 | if(self.stages[stage].stageIndex < self.stages[activeStage].stageIndex){ 66 | // do stuff for EARLIER stages than the selected one 67 | // mark stage navel as previous 68 | self.stages[stage].navEl.classList.add("previous"); 69 | self.stages[stage].navEl.classList.remove("future"); 70 | } 71 | else if(self.stages[stage].stageIndex > self.stages[activeStage].stageIndex){ 72 | // do stuff for LATER stages than the selected one 73 | // mark stage navel as future 74 | self.stages[stage].navEl.classList.remove("previous"); 75 | self.stages[stage].navEl.classList.add("future"); 76 | } 77 | else{ 78 | // do stuff for THE CURRENT stage 79 | // mark as active 80 | self.stages[stage].navEl.classList.remove("previous"); 81 | self.stages[stage].navEl.classList.remove("future"); 82 | self.stages[stage].navEl.classList.add("active"); 83 | } 84 | } 85 | } 86 | } 87 | // show a particular stage 88 | showStage = function(stage){ 89 | stageEls[stage].classList.remove("dn"); 90 | updateStageNav(stage); 91 | // scroll to top 92 | document.body.scrollIntoView(); 93 | } 94 | // hide all stages 95 | hideOtherStages(); 96 | // show the selected stage 97 | showStage(stage); 98 | 99 | // in addition to unhiding the stagee's main container in the showStage function, we trigger the stage's correspdoning controller in order to execute all required javascript for that stage. 100 | // when the controllers are triggered, we pass DOM elements to them that will contain the outputs from the various templates used in that stage. 101 | switch(stage){ 102 | case "industry": 103 | self.industry_list_controller(document.getElementById("industry")); 104 | break; 105 | case "company": 106 | self.company_list_controller( 107 | document.getElementById("company"), 108 | document.getElementById("custom_form"), 109 | document.getElementById("custom_toggle") 110 | ); 111 | break; 112 | case "information": 113 | self.info_categories_form_controller(document.getElementById("info_categories"), document.getElementById("info_category_select_button"), 114 | document.getElementById("information_company_name"), 115 | document.getElementById("information_company_name2")); 116 | break; 117 | case "identifiers": 118 | self.personal_identifiers_form_controller( 119 | document.getElementById("personal_identifiers"), 120 | document.getElementById("personal_identifiers_button"), 121 | document.getElementById("identifiers_company_name") 122 | ); 123 | break; 124 | case "request": 125 | self.requestLetterController( 126 | { 127 | "company_name": document.getElementById('request_company_name'), 128 | "date": document.getElementById('request_date'), 129 | "contact": document.getElementById('request_company_contact'), 130 | "info_categories": document.getElementById('request_information_categories'), 131 | "personal_identifiers": document.getElementById('request_personal_identifiers'), 132 | "signature": document.getElementById('request_signature'), 133 | "address": document.getElementById('request_mailing_address'), 134 | "email": document.getElementById('request_email_address') 135 | }, 136 | document.getElementById('request'), 137 | document.getElementById('request_pdf_button'), 138 | document.getElementById('request_email_button'), 139 | document.getElementById('method_title_both'), 140 | document.getElementById('method_title_mail_only'), 141 | document.getElementById('method_title_email_only'), 142 | document.getElementById('mail_method'), 143 | document.getElementById('email_method'), 144 | document.getElementById('request_copy_button'), 145 | document.getElementById('request_gmail_button') 146 | ); 147 | break; 148 | } 149 | } 150 | // wire up the stage top level nav to show stages when the stage's navEl is clicked, but only if the stage is enabled. 151 | amiApp.stageListController = function(){ 152 | var self = this; 153 | for(var stage in self.stages){ 154 | var exec = function(){ 155 | var this_stage = stage; 156 | self.stages[stage].navEl.onclick = function(e){ 157 | if(self.stages[this_stage].enabled){ 158 | self.showStage(this_stage); 159 | } 160 | } 161 | }(); 162 | } 163 | } -------------------------------------------------------------------------------- /static/js/ami_app/stats_helpers.js: -------------------------------------------------------------------------------- 1 | 2 | // send request to stats server, based on contents of #requestForm 3 | var httpRequest; 4 | function makePostRequest(url) { 5 | httpRequest = new XMLHttpRequest(); 6 | 7 | if (!httpRequest) { 8 | console.log('Giving up :( Cannot create an XMLHTTP instance'); 9 | return false; 10 | } 11 | httpRequest.onreadystatechange = logContents; 12 | httpRequest.open('POST', url); 13 | var formData = new FormData(document.getElementById("requestForm")); 14 | httpRequest.send(formData); 15 | } 16 | function logContents() { 17 | if (httpRequest.readyState === XMLHttpRequest.DONE) { 18 | var msg = JSON.parse(httpRequest.responseText); 19 | var msgHolder = document.getElementById("serverResponse"); 20 | msgHolder.innerHTML = msg.msg; 21 | console.log(msg); 22 | if(httpRequest.status == 200){ 23 | amiApp.stats.requestSent = true; 24 | } 25 | } 26 | } 27 | 28 | function getSearchParameters() { 29 | var prmstr = window.location.search.substr(1); 30 | return prmstr != null && prmstr != "" ? transformToAssocArray(prmstr) : {}; 31 | } 32 | 33 | function transformToAssocArray( prmstr ) { 34 | var params = {}; 35 | var prmarr = prmstr.split("&"); 36 | for ( var i = 0; i < prmarr.length; i++) { 37 | var tmparr = prmarr[i].split("="); 38 | params[tmparr[0]] = tmparr[1]; 39 | } 40 | return params; 41 | } 42 | 43 | // Check for the existnace of a debug url flag 44 | amiApp.urlParams = getSearchParameters(); 45 | if(amiApp.urlParams.hasOwnProperty("debug")){ 46 | // if urlflag is set, then make the request form visible for debugging. 47 | document.getElementById("requestForm").classList.remove("dn"); 48 | } -------------------------------------------------------------------------------- /static/js/ami_app/template_engine.js: -------------------------------------------------------------------------------- 1 | // Here be nasty home-brewed template system. 2 | amiApp.createFromTemplate = function(template_id, data){ 3 | var self = this; 4 | var template = {}; 5 | template.el = self.getTemplate(template_id); 6 | template.valueHolders = self.getTemplateValueHolders(template.el); 7 | for (var property in template.valueHolders) { 8 | if (template.valueHolders.hasOwnProperty(property)) { 9 | if(data.hasOwnProperty(property)){ 10 | template.valueHolders[property].innerHTML = data[property]; 11 | } 12 | } 13 | } 14 | template.imageHolders = self.getTemplateImageHolders(template.el); 15 | for (var property in template.imageHolders) { 16 | if (template.imageHolders.hasOwnProperty(property)) { 17 | if(data.hasOwnProperty(property)){ 18 | template.imageHolders[property].setAttribute('src', data[property]); 19 | } 20 | } 21 | } 22 | template.conditionalValueHolders = self.getTemplateConditionalValueHolders(template.el); 23 | for (var property in template.conditionalValueHolders) { 24 | if (template.conditionalValueHolders.hasOwnProperty(property)) { 25 | // console.log(data[property], typeof data[property] !== "undefined"); 26 | template.valueHolders[property] = template.conditionalValueHolders[property]; 27 | if(data.hasOwnProperty(property) && typeof data[property] !== "undefined" && data[property] !== false){ 28 | template.valueHolders[property].innerHTML = data[property]; 29 | } 30 | else{ 31 | template.valueHolders[property].innerHTML = ""; 32 | } 33 | } 34 | } 35 | return template.el; 36 | } 37 | amiApp.getTemplate = function(templateid){ 38 | var self = this; 39 | 40 | if(!self.templates.hasOwnProperty(templateid)){ 41 | var template = document.querySelector('*[ami_template_id="'+templateid+'"]'); 42 | self.templates[templateid] = template.outerHTML; 43 | template.outerHTML = ""; 44 | } 45 | var el = document.createElement('div'); 46 | el.innerHTML = self.templates[templateid]; 47 | el = el.firstChild; 48 | return el; 49 | } 50 | amiApp.getTemplateValueHolders = function(template){ 51 | var valueHolderEls = template.querySelectorAll('*[ami_template_value_container]'); 52 | var valueHolders = {}; 53 | for (var i = 0; i < valueHolderEls.length; i++) { 54 | key = valueHolderEls[i].getAttribute('ami_template_value_container'); 55 | valueHolders[key] = valueHolderEls[i]; 56 | } 57 | return valueHolders; 58 | } 59 | amiApp.getTemplateImageHolders = function(template){ 60 | var imageHolderEls = template.querySelectorAll('*[ami_template_image_container]'); 61 | var imageHolders = {}; 62 | for (var i = 0; i < imageHolderEls.length; i++) { 63 | key = imageHolderEls[i].getAttribute('ami_template_image_container'); 64 | imageHolders[key] = imageHolderEls[i]; 65 | } 66 | return imageHolders; 67 | } 68 | amiApp.getTemplateConditionalValueHolders = function(template){ 69 | var valueHolderEls = template.querySelectorAll('*[ami_template_value_container_conditional]'); 70 | var valueHolders = {}; 71 | for (var i = 0; i < valueHolderEls.length; i++) { 72 | key = valueHolderEls[i].getAttribute('ami_template_value_container_conditional'); 73 | valueHolders[key] = valueHolderEls[i]; 74 | } 75 | return valueHolders; 76 | } -------------------------------------------------------------------------------- /static/js/canvasDoc.js: -------------------------------------------------------------------------------- 1 | function CanvasPage(pageSize, margins, dpiFactor){ 2 | var page = {} 3 | page.canvas = document.createElement('canvas'); 4 | page.ctx = page.canvas.getContext('2d'); 5 | /// set canvas size representing 300 DPI 6 | page.canvas.width = pageSize.width; 7 | page.canvas.height = pageSize.height; 8 | 9 | /// scale all content to fit the 96 DPI display (DPI doesn't really matter here) 10 | page.canvas.style.width = pageSize.width/dpiFactor/4 + 'px'; 11 | page.canvas.style.height = pageSize.height/dpiFactor/4 + 'px'; 12 | page.canvas.style.margin = 10 + 'px'; 13 | page.canvas.style.border = "1px solid black"; 14 | 15 | page.paintPosition = { 16 | x: margins.left, 17 | y: margins.top 18 | } 19 | 20 | page.maxPaintPosition = { 21 | x: pageSize.width - margins.left - margins.right, 22 | y: pageSize.height - margins.bottom 23 | }; 24 | 25 | fontSize = (40 * dpiFactor).toFixed(0); 26 | page.fontFamily = "Times New Roman, serif" 27 | page.lineHeight = fontSize * 1.5; 28 | 29 | page.ctx.font = fontSize + 'px' + " " + page.fontFamily; 30 | page.ctx.fillStyle = '#000'; 31 | 32 | page.addBackground = function(){ 33 | destinationCanvas = document.createElement("canvas"); 34 | destinationCanvas.width = pageSize.width; 35 | destinationCanvas.height = pageSize.height; 36 | destinationCanvas.style = page.canvas.style; 37 | 38 | destCtx = destinationCanvas.getContext('2d'); 39 | 40 | //create a rectangle with the desired color 41 | destCtx.fillStyle = "#ffffff"; 42 | destCtx.fillRect(0,0,pageSize.width,pageSize.height); 43 | 44 | //draw the original canvas onto the destination canvas 45 | destCtx.drawImage(page.canvas, 0, 0); 46 | page.canvas = destinationCanvas; 47 | page.ctx = destCtx; 48 | page.canvas.style.width = pageSize.width/dpiFactor/4 + 'px'; 49 | page.canvas.style.height = pageSize.height/dpiFactor/4 + 'px'; 50 | } 51 | 52 | page.wrapText = function(text, options) { 53 | var continuing = false; 54 | if(typeof text == "string"){ 55 | var words = textToWords(text); 56 | } 57 | else if(typeof text[0] !== "undefined"){ 58 | var words = text; 59 | continuing = true; 60 | } 61 | 62 | var maxWidth = page.maxPaintPosition.x; 63 | var xPos = this.paintPosition.x; 64 | 65 | var line = ''; 66 | 67 | var listItem = false; 68 | var bottomMargin = page.lineHeight*2; 69 | 70 | if(typeof options !== "undefined"){ 71 | if(options.listItem && !continuing){ 72 | listItem = true; 73 | line = options.listSymbol; 74 | maxWidth -= 45; 75 | xPos += 45; 76 | } 77 | if(options.listItem && continuing){ 78 | listItem = true; 79 | maxWidth -= 45+40; 80 | xPos += 45+50; 81 | } 82 | if(options.noBottomMargin){ 83 | bottomMargin = page.lineHeight; 84 | } 85 | } 86 | firstNewLineDone = false; 87 | 88 | for(var n = 0; n < words.length; n++) { 89 | var testLine = line + words[n] + ''; 90 | var metrics = this.ctx.measureText(testLine); 91 | var testWidth = metrics.width; 92 | if (testWidth > maxWidth && n > 0) { 93 | this.ctx.fillText(line, xPos, this.paintPosition.y); 94 | if(listItem && !firstNewLineDone && !continuing){ 95 | 96 | maxWidth -= 42; 97 | xPos += 42; 98 | firstNewLineDone = true; 99 | } 100 | line = words[n] + ''; 101 | this.paintPosition.y += page.lineHeight; 102 | } 103 | else { 104 | line = testLine; 105 | } 106 | if(this.paintPosition.y >= page.maxPaintPosition.y){ 107 | return words.slice(n); 108 | } 109 | } 110 | this.ctx.fillText(line, xPos, this.paintPosition.y); 111 | this.paintPosition.y += bottomMargin; 112 | return null; 113 | } 114 | 115 | return page; 116 | } 117 | 118 | function CanvasDocument(paperType, margins){ 119 | var self = this; 120 | self.pages = []; 121 | 122 | // Paper types expressed as pixel values at 300ppi 123 | // self.paperTypes = { 124 | // "letter": {width: 2550, height: 3300}, 125 | // "legal": {width: 2550, height: 4200}, 126 | // "A4": {width: 2480, height: 3508} 127 | // } 128 | 129 | self.paperTypes = { 130 | "letter": {width: 2550/(3.125/2), height: 3300/(3.125/2), pdfSize: {width: 612, height: 792}}, 131 | "legal": {width: 2550/(3.125/2), height: 4200/(3.125/2), pdfSize: {width: 612, height: 1008}}, 132 | "A4": {width: 2480/(3.125/2), height: 3508/(3.125/2), pdfSize: {width: 595.28, height: 841.89}} 133 | } 134 | try{ 135 | self.paperType = self.paperTypes[paperType]; 136 | self.paperType.width = self.paperType.width; 137 | self.paperType.height = self.paperType.height; 138 | } 139 | catch(e){ 140 | throw new Error("Paper type " + paperType + " not supported"); 141 | } 142 | 143 | // DPI factor sets it to 300 relative to default 96 144 | // var dpiFactor = 300 / 96; 145 | var dpiFactor = 1; 146 | 147 | var marginCalc = function(pageSize, margins){ 148 | return { 149 | top: pageSize.width * margins[0] / 100, 150 | right: pageSize.width * margins[1] / 100, 151 | bottom: pageSize.width * margins[2] / 100, 152 | left: pageSize.width * margins[3] / 100 153 | } 154 | } 155 | self.activePageIndex = 0; 156 | 157 | self.getActivePageIndex = function(){ 158 | return self.activePageIndex; 159 | } 160 | 161 | // Set page widths in pixels relative to DPI factor 162 | self.pageSize = { 163 | width: self.paperType.width * dpiFactor, 164 | height: self.paperType.height * dpiFactor 165 | } 166 | self.margins = marginCalc(self.pageSize, margins); 167 | 168 | // Add first page 169 | self.pages.push(new CanvasPage(self.pageSize, self.margins, dpiFactor)); 170 | 171 | self.writeText = function(text, options){ 172 | activePage = self.pages[self.getActivePageIndex()]; 173 | text = activePage.wrapText(text, options); 174 | if(text == null){ 175 | return; 176 | } 177 | else{ 178 | console.log('new page') 179 | // Add new page, and keep writing 180 | self.pages.push(new CanvasPage(self.pageSize, self.margins, dpiFactor)); 181 | self.activePageIndex += 1; 182 | self.writeText(text, options); 183 | } 184 | } 185 | 186 | self.parseHTMLBlockLevelElements = function(containerEl, selector){ 187 | // strip out inline elements, the non-regex way 188 | els = containerEl.querySelectorAll('span, strong, i, em, bold, big, small, tt, abbr, acronym, cite, code, dfn, em, kbd, strong, samp, time, var, a, bdo, br, img, map, object, q, script, span, sub, sup, button, input, label, select, textarea'); 189 | 190 | for(var i=els.length-1; i >= 0; i--){ 191 | var el = els[i]; 192 | text = document.createTextNode(el.innerText); 193 | el.parentNode.replaceChild(text, el); 194 | } 195 | 196 | nodes = self.getDescendants(containerEl); 197 | return nodes; 198 | } 199 | self.getDescendants = function(node, accum, textNodes) { 200 | var i; 201 | accum = accum || []; 202 | for (i = 0; i < node.children.length; i++) { 203 | accum.push({el: node.children[i], parentTagName: node.tagName, tagName: node.tagName}); 204 | self.getDescendants(node.children[i], accum); 205 | } 206 | return accum; 207 | } 208 | 209 | self.parseEl = function(el){ 210 | var pdfContent = []; 211 | nodes = self.parseHTMLBlockLevelElements(el); 212 | 213 | for(var i=0; i < nodes.length; i++){ 214 | // Check if leaf node 215 | if(nodes[i].el.children.length === 0 && nodes[i].el.innerText.length > 0){ 216 | pdfContent.push({ 217 | 'tag': nodes[i].el.tagName, 218 | 'text': nodes[i].el.innerText, 219 | 'parent': nodes[i].el.parentNode 220 | }); 221 | } 222 | } 223 | for(var i=0; i < pdfContent.length; i++){ 224 | pdfContent[i].options = {}; 225 | if(i>0 && pdfContent[i].tag == "LI" && pdfContent.length && pdfContent[i-1].tag !== "LI"){ 226 | list_position = 0; 227 | } 228 | if(pdfContent[i].tag == "LI"){ 229 | pdfContent[i].options.listItem = true; 230 | if(pdfContent[i].parent.tagName == "OL"){ 231 | pdfContent[i].options.listSymbol = list_position+1+". "; 232 | } 233 | else{ 234 | pdfContent[i].options.listSymbol = "• "; 235 | } 236 | if(pdfContent[i].parent.tagName == "OL" && pdfContent[i].parent.getAttribute("type") == "A"){ 237 | pdfContent[i].options.listSymbol = String.fromCharCode(97 + list_position).toUpperCase()+". "; 238 | } 239 | if(i+1 < pdfContent.length && pdfContent[i+1].tag == "LI"){ 240 | pdfContent[i].options.noBottomMargin = true; 241 | list_position++ 242 | } 243 | } 244 | else if(pdfContent[i].tag == "DIV"){ 245 | if(i+1 < pdfContent.length && pdfContent[i+1].tag == "DIV"){ 246 | pdfContent[i].options.noBottomMargin = true; 247 | } 248 | } 249 | } 250 | return pdfContent; 251 | } 252 | self.writeHTMLtoDoc = function(el){ 253 | pdfContent = self.parseEl(el); 254 | for(var i=0; i < pdfContent.length; i++){ 255 | self.writeText(pdfContent[i].text, pdfContent[i].options); 256 | } 257 | for (var i = 0; i < self.pages.length; i++) { 258 | self.pages[i].addBackground(); 259 | self.pages[i].dataURL = self.pages[i].canvas.toDataURL("image/jpeg"); 260 | } 261 | } 262 | 263 | self.createPDF = function(){ 264 | var dd = { 265 | pageSize: paperType, 266 | content: [], 267 | pageMargins: 1 268 | } 269 | // Add our canvas pages to the PDF, scale to size 270 | for (var i = 0; i < self.pages.length; i++) { 271 | console.log("adding page " + i); 272 | dd.content.push({ 273 | image: self.pages[i].dataURL, 274 | width: self.paperType.pdfSize.width-(dd.pageMargins*2), 275 | alignment: 'center' 276 | }) 277 | } 278 | self.pdf = createPdf(dd); 279 | } 280 | self.openPDF = function(){ 281 | self.pdf.open(); 282 | } 283 | self.savePDF = function(filename){ 284 | self.pdf.download(filename); 285 | } 286 | } 287 | function textToWords(text){ 288 | var re = /[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g 289 | var characters = text.match(re); 290 | 291 | var el = document.createElement('div'); 292 | el.style.width = 0; 293 | el.style.position = 'absolute'; 294 | el.style.visibility = 'hidden'; 295 | var oldHeight = 0; 296 | var newHeight; 297 | var words = []; 298 | var currentWordIndex = -1; 299 | 300 | document.body.appendChild(el); 301 | 302 | for (var i = 0; i < characters.length; i++) { 303 | char = characters[i]; 304 | oldHeight = el.offsetHeight; 305 | el.innerHTML += char; 306 | newHeight = el.offsetHeight; 307 | if(newHeight > oldHeight){ 308 | //new array item 309 | words.push(char) 310 | currentWordIndex++; 311 | el.innerText = char; 312 | newHeight = 0; 313 | } 314 | else{ 315 | //append to same word 316 | words[currentWordIndex] += char; 317 | } 318 | } 319 | return words; 320 | } -------------------------------------------------------------------------------- /static/js/vendor/modernizr-3.7.1.min.js: -------------------------------------------------------------------------------- 1 | /*! modernizr 3.7.1 (Custom Build) | MIT * 2 | * https://modernizr.com/download/?-cssanimations-csscolumns-customelements-flexbox-history-picture-pointerevents-postmessage-sizes-srcset-webgl-websockets-webworkers-addtest-domprefixes-hasevent-mq-prefixedcssvalue-prefixes-setclasses-testallprops-testprop-teststyles !*/ 3 | !function(e,t,n){function r(e,t){return typeof e===t}function o(e){var t=b.className,n=Modernizr._config.classPrefix||"";if(S&&(t=t.baseVal),Modernizr._config.enableJSClass){var r=new RegExp("(^|\\s)"+n+"no-js(\\s|$)");t=t.replace(r,"$1"+n+"js$2")}Modernizr._config.enableClasses&&(e.length>0&&(t+=" "+n+e.join(" "+n)),S?b.className.baseVal=t:b.className=t)}function i(e,t){if("object"==typeof e)for(var n in e)E(e,n)&&i(n,e[n]);else{e=e.toLowerCase();var r=e.split("."),s=Modernizr[r[0]];if(2===r.length&&(s=s[r[1]]),void 0!==s)return Modernizr;t="function"==typeof t?t():t,1===r.length?Modernizr[r[0]]=t:(!Modernizr[r[0]]||Modernizr[r[0]]instanceof Boolean||(Modernizr[r[0]]=new Boolean(Modernizr[r[0]])),Modernizr[r[0]][r[1]]=t),o([(t&&!1!==t?"":"no-")+r.join("-")]),Modernizr._trigger(e,t)}return Modernizr}function s(){return"function"!=typeof t.createElement?t.createElement(arguments[0]):S?t.createElementNS.call(t,"http://www.w3.org/2000/svg",arguments[0]):t.createElement.apply(t,arguments)}function a(){var e=t.body;return e||(e=s(S?"svg":"body"),e.fake=!0),e}function l(e,n,r,o){var i,l,u,f,c="modernizr",d=s("div"),p=a();if(parseInt(r,10))for(;r--;)u=s("div"),u.id=o?o[r]:c+(r+1),d.appendChild(u);return i=s("style"),i.type="text/css",i.id="s"+c,(p.fake?p:d).appendChild(i),p.appendChild(d),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(t.createTextNode(e)),d.id=c,p.fake&&(p.style.background="",p.style.overflow="hidden",f=b.style.overflow,b.style.overflow="hidden",b.appendChild(p)),l=n(d,e),p.fake?(p.parentNode.removeChild(p),b.style.overflow=f,b.offsetHeight):d.parentNode.removeChild(d),!!l}function u(e,t){return!!~(""+e).indexOf(t)}function f(e){return e.replace(/([A-Z])/g,function(e,t){return"-"+t.toLowerCase()}).replace(/^ms-/,"-ms-")}function c(t,n,r){var o;if("getComputedStyle"in e){o=getComputedStyle.call(e,t,n);var i=e.console;if(null!==o)r&&(o=o.getPropertyValue(r));else if(i){var s=i.error?"error":"log";i[s].call(i,"getComputedStyle returning null, its possible modernizr test results are inaccurate")}}else o=!n&&t.currentStyle&&t.currentStyle[r];return o}function d(t,r){var o=t.length;if("CSS"in e&&"supports"in e.CSS){for(;o--;)if(e.CSS.supports(f(t[o]),r))return!0;return!1}if("CSSSupportsRule"in e){for(var i=[];o--;)i.push("("+f(t[o])+":"+r+")");return i=i.join(" or "),l("@supports ("+i+") { #modernizr { position: absolute; } }",function(e){return"absolute"===c(e,null,"position")})}return n}function p(e){return e.replace(/([a-z])-([a-z])/g,function(e,t,n){return t+n.toUpperCase()}).replace(/^-/,"")}function m(e,t,o,i){function a(){f&&(delete L.style,delete L.modElem)}if(i=!r(i,"undefined")&&i,!r(o,"undefined")){var l=d(e,o);if(!r(l,"undefined"))return l}for(var f,c,m,h,v,A=["modernizr","tspan","samp"];!L.style&&A.length;)f=!0,L.modElem=s(A.shift()),L.style=L.modElem.style;for(m=e.length,c=0;cn;n++)r(e,e._deferreds[n]);e._deferreds=null}function c(e,n){var t=!1;try{e(function(e){t||(t=!0,i(n,e))},function(e){t||(t=!0,f(n,e))})}catch(o){if(t)return;t=!0,f(n,o)}}var a=setTimeout;o.prototype["catch"]=function(e){return this.then(null,e)},o.prototype.then=function(e,n){var o=new this.constructor(t);return r(this,new function(e,n,t){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof n?n:null,this.promise=t}(e,n,o)),o},o.prototype["finally"]=e,o.all=function(e){return new o(function(t,o){function r(e,n){try{if(n&&("object"==typeof n||"function"==typeof n)){var u=n.then;if("function"==typeof u)return void u.call(n,function(n){r(e,n)},o)}i[e]=n,0==--f&&t(i)}catch(c){o(c)}}if(!n(e))return o(new TypeError("Promise.all accepts an array"));var i=Array.prototype.slice.call(e);if(0===i.length)return t([]);for(var f=i.length,u=0;i.length>u;u++)r(u,i[u])})},o.resolve=function(e){return e&&"object"==typeof e&&e.constructor===o?e:new o(function(n){n(e)})},o.reject=function(e){return new o(function(n,t){t(e)})},o.race=function(e){return new o(function(t,r){if(!n(e))return r(new TypeError("Promise.race accepts an array"));for(var i=0,f=e.length;f>i;i++)o.resolve(e[i]).then(t,r)})},o._immediateFn="function"==typeof setImmediate&&function(e){setImmediate(e)}||function(e){a(e,0)},o._unhandledRejectionFn=function(e){void 0!==console&&console&&console.warn("Possible Unhandled Promise Rejection:",e)};var l=function(){if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if("undefined"!=typeof global)return global;throw Error("unable to locate global object")}();"Promise"in l?l.Promise.prototype["finally"]||(l.Promise.prototype["finally"]=e):l.Promise=o}); 2 | -------------------------------------------------------------------------------- /static/js/vendor/textencoder-polyfill.js: -------------------------------------------------------------------------------- 1 | // from https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder#Polyfill 2 | if (typeof TextEncoder === "undefined") { 3 | TextEncoder=function TextEncoder(){}; 4 | TextEncoder.prototype.encode = function encode(str) { 5 | "use strict"; 6 | var Len = str.length, resPos = -1; 7 | // The Uint8Array's length must be at least 3x the length of the string because an invalid UTF-16 8 | // takes up the equivelent space of 3 UTF-8 characters to encode it properly. However, Array's 9 | // have an auto expanding length and 1.5x should be just the right balance for most uses. 10 | var resArr = typeof Uint8Array === "undefined" ? new Array(Len * 1.5) : new Uint8Array(Len * 3); 11 | for (var point=0, nextcode=0, i = 0; i !== Len; ) { 12 | point = str.charCodeAt(i), i += 1; 13 | if (point >= 0xD800 && point <= 0xDBFF) { 14 | if (i === Len) { 15 | resArr[resPos += 1] = 0xef/*0b11101111*/; resArr[resPos += 1] = 0xbf/*0b10111111*/; 16 | resArr[resPos += 1] = 0xbd/*0b10111101*/; break; 17 | } 18 | // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae 19 | nextcode = str.charCodeAt(i); 20 | if (nextcode >= 0xDC00 && nextcode <= 0xDFFF) { 21 | point = (point - 0xD800) * 0x400 + nextcode - 0xDC00 + 0x10000; 22 | i += 1; 23 | if (point > 0xffff) { 24 | resArr[resPos += 1] = (0x1e/*0b11110*/<<3) | (point>>>18); 25 | resArr[resPos += 1] = (0x2/*0b10*/<<6) | ((point>>>12)&0x3f/*0b00111111*/); 26 | resArr[resPos += 1] = (0x2/*0b10*/<<6) | ((point>>>6)&0x3f/*0b00111111*/); 27 | resArr[resPos += 1] = (0x2/*0b10*/<<6) | (point&0x3f/*0b00111111*/); 28 | continue; 29 | } 30 | } else { 31 | resArr[resPos += 1] = 0xef/*0b11101111*/; resArr[resPos += 1] = 0xbf/*0b10111111*/; 32 | resArr[resPos += 1] = 0xbd/*0b10111101*/; continue; 33 | } 34 | } 35 | if (point <= 0x007f) { 36 | resArr[resPos += 1] = (0x0/*0b0*/<<7) | point; 37 | } else if (point <= 0x07ff) { 38 | resArr[resPos += 1] = (0x6/*0b110*/<<5) | (point>>>6); 39 | resArr[resPos += 1] = (0x2/*0b10*/<<6) | (point&0x3f/*0b00111111*/); 40 | } else { 41 | resArr[resPos += 1] = (0xe/*0b1110*/<<4) | (point>>>12); 42 | resArr[resPos += 1] = (0x2/*0b10*/<<6) | ((point>>>6)&0x3f/*0b00111111*/); 43 | resArr[resPos += 1] = (0x2/*0b10*/<<6) | (point&0x3f/*0b00111111*/); 44 | } 45 | } 46 | if (typeof Uint8Array !== "undefined") return resArr.subarray(0, resPos + 1); 47 | // else // IE 6-9 48 | resArr.length = resPos + 1; // trim off extra weight 49 | return resArr; 50 | }; 51 | TextEncoder.prototype.toString = function(){return "[object TextEncoder]"}; 52 | try { // Object.defineProperty only works on DOM prototypes in IE8 53 | Object.defineProperty(TextEncoder.prototype,"encoding",{ 54 | get:function(){if(TextEncoder.prototype.isPrototypeOf(this)) return"utf-8"; 55 | else throw TypeError("Illegal invocation");} 56 | }); 57 | } catch(e) { /*IE6-8 fallback*/ TextEncoder.prototype.encoding = "utf-8"; } 58 | if(typeof Symbol!=="undefined")TextEncoder.prototype[Symbol.toStringTag]="TextEncoder"; 59 | } -------------------------------------------------------------------------------- /static/js/vendor/webcrypto-shim.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Web Cryptography API shim 3 | * @author Artem S Vybornov 4 | * @license MIT 5 | */ 6 | (function (global, factory) { 7 | if (typeof define === 'function' && define.amd) { 8 | // AMD. Register as an anonymous module. 9 | define([], function () { 10 | return factory(global); 11 | }); 12 | } else if (typeof module === 'object' && module.exports) { 13 | // CommonJS-like environments that support module.exports 14 | module.exports = factory(global); 15 | } else { 16 | factory(global); 17 | } 18 | }(typeof self !== 'undefined' ? self : this, function (global) { 19 | 'use strict'; 20 | 21 | if ( typeof Promise !== 'function' ) 22 | throw "Promise support required"; 23 | 24 | var _crypto = global.crypto || global.msCrypto; 25 | if ( !_crypto ) return; 26 | 27 | var _subtle = _crypto.subtle || _crypto.webkitSubtle; 28 | if ( !_subtle ) return; 29 | 30 | var _Crypto = global.Crypto || _crypto.constructor || Object, 31 | _SubtleCrypto = global.SubtleCrypto || _subtle.constructor || Object, 32 | _CryptoKey = global.CryptoKey || global.Key || Object; 33 | 34 | var isEdge = global.navigator.userAgent.indexOf('Edge/') > -1; 35 | var isIE = !!global.msCrypto && !isEdge; 36 | var isWebkit = !_crypto.subtle && !!_crypto.webkitSubtle; 37 | if ( !isIE && !isWebkit ) return; 38 | 39 | function s2a ( s ) { 40 | return btoa(s).replace(/\=+$/, '').replace(/\+/g, '-').replace(/\//g, '_'); 41 | } 42 | 43 | function a2s ( s ) { 44 | s += '===', s = s.slice( 0, -s.length % 4 ); 45 | return atob( s.replace(/-/g, '+').replace(/_/g, '/') ); 46 | } 47 | 48 | function s2b ( s ) { 49 | var b = new Uint8Array(s.length); 50 | for ( var i = 0; i < s.length; i++ ) b[i] = s.charCodeAt(i); 51 | return b; 52 | } 53 | 54 | function b2s ( b ) { 55 | if ( b instanceof ArrayBuffer ) b = new Uint8Array(b); 56 | return String.fromCharCode.apply( String, b ); 57 | } 58 | 59 | function alg ( a ) { 60 | var r = { 'name': (a.name || a || '').toUpperCase().replace('V','v') }; 61 | switch ( r.name ) { 62 | case 'SHA-1': 63 | case 'SHA-256': 64 | case 'SHA-384': 65 | case 'SHA-512': 66 | break; 67 | case 'AES-CBC': 68 | case 'AES-GCM': 69 | case 'AES-KW': 70 | if ( a.length ) r['length'] = a.length; 71 | break; 72 | case 'HMAC': 73 | if ( a.hash ) r['hash'] = alg(a.hash); 74 | if ( a.length ) r['length'] = a.length; 75 | break; 76 | case 'RSAES-PKCS1-v1_5': 77 | if ( a.publicExponent ) r['publicExponent'] = new Uint8Array(a.publicExponent); 78 | if ( a.modulusLength ) r['modulusLength'] = a.modulusLength; 79 | break; 80 | case 'RSASSA-PKCS1-v1_5': 81 | case 'RSA-OAEP': 82 | if ( a.hash ) r['hash'] = alg(a.hash); 83 | if ( a.publicExponent ) r['publicExponent'] = new Uint8Array(a.publicExponent); 84 | if ( a.modulusLength ) r['modulusLength'] = a.modulusLength; 85 | break; 86 | default: 87 | throw new SyntaxError("Bad algorithm name"); 88 | } 89 | return r; 90 | }; 91 | 92 | function jwkAlg ( a ) { 93 | return { 94 | 'HMAC': { 95 | 'SHA-1': 'HS1', 96 | 'SHA-256': 'HS256', 97 | 'SHA-384': 'HS384', 98 | 'SHA-512': 'HS512', 99 | }, 100 | 'RSASSA-PKCS1-v1_5': { 101 | 'SHA-1': 'RS1', 102 | 'SHA-256': 'RS256', 103 | 'SHA-384': 'RS384', 104 | 'SHA-512': 'RS512', 105 | }, 106 | 'RSAES-PKCS1-v1_5': { 107 | '': 'RSA1_5', 108 | }, 109 | 'RSA-OAEP': { 110 | 'SHA-1': 'RSA-OAEP', 111 | 'SHA-256': 'RSA-OAEP-256', 112 | }, 113 | 'AES-KW': { 114 | '128': 'A128KW', 115 | '192': 'A192KW', 116 | '256': 'A256KW', 117 | }, 118 | 'AES-GCM': { 119 | '128': 'A128GCM', 120 | '192': 'A192GCM', 121 | '256': 'A256GCM', 122 | }, 123 | 'AES-CBC': { 124 | '128': 'A128CBC', 125 | '192': 'A192CBC', 126 | '256': 'A256CBC', 127 | }, 128 | }[a.name][ ( a.hash || {} ).name || a.length || '' ]; 129 | } 130 | 131 | function b2jwk ( k ) { 132 | if ( k instanceof ArrayBuffer || k instanceof Uint8Array ) k = JSON.parse( decodeURIComponent( escape( b2s(k) ) ) ); 133 | var jwk = { 'kty': k.kty, 'alg': k.alg, 'ext': k.ext || k.extractable }; 134 | switch ( jwk.kty ) { 135 | case 'oct': 136 | jwk.k = k.k; 137 | case 'RSA': 138 | [ 'n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi', 'oth' ].forEach( function ( x ) { if ( x in k ) jwk[x] = k[x] } ); 139 | break; 140 | default: 141 | throw new TypeError("Unsupported key type"); 142 | } 143 | return jwk; 144 | } 145 | 146 | function jwk2b ( k ) { 147 | var jwk = b2jwk(k); 148 | if ( isIE ) jwk['extractable'] = jwk.ext, delete jwk.ext; 149 | return s2b( unescape( encodeURIComponent( JSON.stringify(jwk) ) ) ).buffer; 150 | } 151 | 152 | function pkcs2jwk ( k ) { 153 | var info = b2der(k), prv = false; 154 | if ( info.length > 2 ) prv = true, info.shift(); // remove version from PKCS#8 PrivateKeyInfo structure 155 | var jwk = { 'ext': true }; 156 | switch ( info[0][0] ) { 157 | case '1.2.840.113549.1.1.1': 158 | var rsaComp = [ 'n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi' ], 159 | rsaKey = b2der( info[1] ); 160 | if ( prv ) rsaKey.shift(); // remove version from PKCS#1 RSAPrivateKey structure 161 | for ( var i = 0; i < rsaKey.length; i++ ) { 162 | if ( !rsaKey[i][0] ) rsaKey[i] = rsaKey[i].subarray(1); 163 | jwk[ rsaComp[i] ] = s2a( b2s( rsaKey[i] ) ); 164 | } 165 | jwk['kty'] = 'RSA'; 166 | break; 167 | default: 168 | throw new TypeError("Unsupported key type"); 169 | } 170 | return jwk; 171 | } 172 | 173 | function jwk2pkcs ( k ) { 174 | var key, info = [ [ '', null ] ], prv = false; 175 | switch ( k.kty ) { 176 | case 'RSA': 177 | var rsaComp = [ 'n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi' ], 178 | rsaKey = []; 179 | for ( var i = 0; i < rsaComp.length; i++ ) { 180 | if ( !( rsaComp[i] in k ) ) break; 181 | var b = rsaKey[i] = s2b( a2s( k[ rsaComp[i] ] ) ); 182 | if ( b[0] & 0x80 ) rsaKey[i] = new Uint8Array(b.length + 1), rsaKey[i].set( b, 1 ); 183 | } 184 | if ( rsaKey.length > 2 ) prv = true, rsaKey.unshift( new Uint8Array([0]) ); // add version to PKCS#1 RSAPrivateKey structure 185 | info[0][0] = '1.2.840.113549.1.1.1'; 186 | key = rsaKey; 187 | break; 188 | default: 189 | throw new TypeError("Unsupported key type"); 190 | } 191 | info.push( new Uint8Array( der2b(key) ).buffer ); 192 | if ( !prv ) info[1] = { 'tag': 0x03, 'value': info[1] }; 193 | else info.unshift( new Uint8Array([0]) ); // add version to PKCS#8 PrivateKeyInfo structure 194 | return new Uint8Array( der2b(info) ).buffer; 195 | } 196 | 197 | var oid2str = { 'KoZIhvcNAQEB': '1.2.840.113549.1.1.1' }, 198 | str2oid = { '1.2.840.113549.1.1.1': 'KoZIhvcNAQEB' }; 199 | 200 | function b2der ( buf, ctx ) { 201 | if ( buf instanceof ArrayBuffer ) buf = new Uint8Array(buf); 202 | if ( !ctx ) ctx = { pos: 0, end: buf.length }; 203 | 204 | if ( ctx.end - ctx.pos < 2 || ctx.end > buf.length ) throw new RangeError("Malformed DER"); 205 | 206 | var tag = buf[ctx.pos++], 207 | len = buf[ctx.pos++]; 208 | 209 | if ( len >= 0x80 ) { 210 | len &= 0x7f; 211 | if ( ctx.end - ctx.pos < len ) throw new RangeError("Malformed DER"); 212 | for ( var xlen = 0; len--; ) xlen <<= 8, xlen |= buf[ctx.pos++]; 213 | len = xlen; 214 | } 215 | 216 | if ( ctx.end - ctx.pos < len ) throw new RangeError("Malformed DER"); 217 | 218 | var rv; 219 | 220 | switch ( tag ) { 221 | case 0x02: // Universal Primitive INTEGER 222 | rv = buf.subarray( ctx.pos, ctx.pos += len ); 223 | break; 224 | case 0x03: // Universal Primitive BIT STRING 225 | if ( buf[ctx.pos++] ) throw new Error( "Unsupported bit string" ); 226 | len--; 227 | case 0x04: // Universal Primitive OCTET STRING 228 | rv = new Uint8Array( buf.subarray( ctx.pos, ctx.pos += len ) ).buffer; 229 | break; 230 | case 0x05: // Universal Primitive NULL 231 | rv = null; 232 | break; 233 | case 0x06: // Universal Primitive OBJECT IDENTIFIER 234 | var oid = btoa( b2s( buf.subarray( ctx.pos, ctx.pos += len ) ) ); 235 | if ( !( oid in oid2str ) ) throw new Error( "Unsupported OBJECT ID " + oid ); 236 | rv = oid2str[oid]; 237 | break; 238 | case 0x30: // Universal Constructed SEQUENCE 239 | rv = []; 240 | for ( var end = ctx.pos + len; ctx.pos < end; ) rv.push( b2der( buf, ctx ) ); 241 | break; 242 | default: 243 | throw new Error( "Unsupported DER tag 0x" + tag.toString(16) ); 244 | } 245 | 246 | return rv; 247 | } 248 | 249 | function der2b ( val, buf ) { 250 | if ( !buf ) buf = []; 251 | 252 | var tag = 0, len = 0, 253 | pos = buf.length + 2; 254 | 255 | buf.push( 0, 0 ); // placeholder 256 | 257 | if ( val instanceof Uint8Array ) { // Universal Primitive INTEGER 258 | tag = 0x02, len = val.length; 259 | for ( var i = 0; i < len; i++ ) buf.push( val[i] ); 260 | } 261 | else if ( val instanceof ArrayBuffer ) { // Universal Primitive OCTET STRING 262 | tag = 0x04, len = val.byteLength, val = new Uint8Array(val); 263 | for ( var i = 0; i < len; i++ ) buf.push( val[i] ); 264 | } 265 | else if ( val === null ) { // Universal Primitive NULL 266 | tag = 0x05, len = 0; 267 | } 268 | else if ( typeof val === 'string' && val in str2oid ) { // Universal Primitive OBJECT IDENTIFIER 269 | var oid = s2b( atob( str2oid[val] ) ); 270 | tag = 0x06, len = oid.length; 271 | for ( var i = 0; i < len; i++ ) buf.push( oid[i] ); 272 | } 273 | else if ( val instanceof Array ) { // Universal Constructed SEQUENCE 274 | for ( var i = 0; i < val.length; i++ ) der2b( val[i], buf ); 275 | tag = 0x30, len = buf.length - pos; 276 | } 277 | else if ( typeof val === 'object' && val.tag === 0x03 && val.value instanceof ArrayBuffer ) { // Tag hint 278 | val = new Uint8Array(val.value), tag = 0x03, len = val.byteLength; 279 | buf.push(0); for ( var i = 0; i < len; i++ ) buf.push( val[i] ); 280 | len++; 281 | } 282 | else { 283 | throw new Error( "Unsupported DER value " + val ); 284 | } 285 | 286 | if ( len >= 0x80 ) { 287 | var xlen = len, len = 4; 288 | buf.splice( pos, 0, (xlen >> 24) & 0xff, (xlen >> 16) & 0xff, (xlen >> 8) & 0xff, xlen & 0xff ); 289 | while ( len > 1 && !(xlen >> 24) ) xlen <<= 8, len--; 290 | if ( len < 4 ) buf.splice( pos, 4 - len ); 291 | len |= 0x80; 292 | } 293 | 294 | buf.splice( pos - 2, 2, tag, len ); 295 | 296 | return buf; 297 | } 298 | 299 | function CryptoKey ( key, alg, ext, use ) { 300 | Object.defineProperties( this, { 301 | _key: { 302 | value: key 303 | }, 304 | type: { 305 | value: key.type, 306 | enumerable: true, 307 | }, 308 | extractable: { 309 | value: (ext === undefined) ? key.extractable : ext, 310 | enumerable: true, 311 | }, 312 | algorithm: { 313 | value: (alg === undefined) ? key.algorithm : alg, 314 | enumerable: true, 315 | }, 316 | usages: { 317 | value: (use === undefined) ? key.usages : use, 318 | enumerable: true, 319 | }, 320 | }); 321 | } 322 | 323 | function isPubKeyUse ( u ) { 324 | return u === 'verify' || u === 'encrypt' || u === 'wrapKey'; 325 | } 326 | 327 | function isPrvKeyUse ( u ) { 328 | return u === 'sign' || u === 'decrypt' || u === 'unwrapKey'; 329 | } 330 | 331 | [ 'generateKey', 'importKey', 'unwrapKey' ] 332 | .forEach( function ( m ) { 333 | var _fn = _subtle[m]; 334 | 335 | _subtle[m] = function ( a, b, c ) { 336 | var args = [].slice.call(arguments), 337 | ka, kx, ku; 338 | 339 | switch ( m ) { 340 | case 'generateKey': 341 | ka = alg(a), kx = b, ku = c; 342 | break; 343 | case 'importKey': 344 | ka = alg(c), kx = args[3], ku = args[4]; 345 | if ( a === 'jwk' ) { 346 | b = b2jwk(b); 347 | if ( !b.alg ) b.alg = jwkAlg(ka); 348 | if ( !b.key_ops ) b.key_ops = ( b.kty !== 'oct' ) ? ( 'd' in b ) ? ku.filter(isPrvKeyUse) : ku.filter(isPubKeyUse) : ku.slice(); 349 | args[1] = jwk2b(b); 350 | } 351 | break; 352 | case 'unwrapKey': 353 | ka = args[4], kx = args[5], ku = args[6]; 354 | args[2] = c._key; 355 | break; 356 | } 357 | 358 | if ( m === 'generateKey' && ka.name === 'HMAC' && ka.hash ) { 359 | ka.length = ka.length || { 'SHA-1': 512, 'SHA-256': 512, 'SHA-384': 1024, 'SHA-512': 1024 }[ka.hash.name]; 360 | return _subtle.importKey( 'raw', _crypto.getRandomValues( new Uint8Array( (ka.length+7)>>3 ) ), ka, kx, ku ); 361 | } 362 | 363 | if ( isWebkit && m === 'generateKey' && ka.name === 'RSASSA-PKCS1-v1_5' && ( !ka.modulusLength || ka.modulusLength >= 2048 ) ) { 364 | a = alg(a), a.name = 'RSAES-PKCS1-v1_5', delete a.hash; 365 | return _subtle.generateKey( a, true, [ 'encrypt', 'decrypt' ] ) 366 | .then( function ( k ) { 367 | return Promise.all([ 368 | _subtle.exportKey( 'jwk', k.publicKey ), 369 | _subtle.exportKey( 'jwk', k.privateKey ), 370 | ]); 371 | }) 372 | .then( function ( keys ) { 373 | keys[0].alg = keys[1].alg = jwkAlg(ka); 374 | keys[0].key_ops = ku.filter(isPubKeyUse), keys[1].key_ops = ku.filter(isPrvKeyUse); 375 | return Promise.all([ 376 | _subtle.importKey( 'jwk', keys[0], ka, true, keys[0].key_ops ), 377 | _subtle.importKey( 'jwk', keys[1], ka, kx, keys[1].key_ops ), 378 | ]); 379 | }) 380 | .then( function ( keys ) { 381 | return { 382 | publicKey: keys[0], 383 | privateKey: keys[1], 384 | }; 385 | }); 386 | } 387 | 388 | if ( ( isWebkit || ( isIE && ( ka.hash || {} ).name === 'SHA-1' ) ) 389 | && m === 'importKey' && a === 'jwk' && ka.name === 'HMAC' && b.kty === 'oct' ) { 390 | return _subtle.importKey( 'raw', s2b( a2s(b.k) ), c, args[3], args[4] ); 391 | } 392 | 393 | if ( isWebkit && m === 'importKey' && ( a === 'spki' || a === 'pkcs8' ) ) { 394 | return _subtle.importKey( 'jwk', pkcs2jwk(b), c, args[3], args[4] ); 395 | } 396 | 397 | if ( isIE && m === 'unwrapKey' ) { 398 | return _subtle.decrypt( args[3], c, b ) 399 | .then( function ( k ) { 400 | return _subtle.importKey( a, k, args[4], args[5], args[6] ); 401 | }); 402 | } 403 | 404 | var op; 405 | try { 406 | op = _fn.apply( _subtle, args ); 407 | } 408 | catch ( e ) { 409 | return Promise.reject(e); 410 | } 411 | 412 | if ( isIE ) { 413 | op = new Promise( function ( res, rej ) { 414 | op.onabort = 415 | op.onerror = function ( e ) { rej(e) }; 416 | op.oncomplete = function ( r ) { res(r.target.result) }; 417 | }); 418 | } 419 | 420 | op = op.then( function ( k ) { 421 | if ( ka.name === 'HMAC' ) { 422 | if ( !ka.length ) ka.length = 8 * k.algorithm.length; 423 | } 424 | if ( ka.name.search('RSA') == 0 ) { 425 | if ( !ka.modulusLength ) ka.modulusLength = (k.publicKey || k).algorithm.modulusLength; 426 | if ( !ka.publicExponent ) ka.publicExponent = (k.publicKey || k).algorithm.publicExponent; 427 | } 428 | if ( k.publicKey && k.privateKey ) { 429 | k = { 430 | publicKey: new CryptoKey( k.publicKey, ka, kx, ku.filter(isPubKeyUse) ), 431 | privateKey: new CryptoKey( k.privateKey, ka, kx, ku.filter(isPrvKeyUse) ), 432 | }; 433 | } 434 | else { 435 | k = new CryptoKey( k, ka, kx, ku ); 436 | } 437 | return k; 438 | }); 439 | 440 | return op; 441 | } 442 | }); 443 | 444 | [ 'exportKey', 'wrapKey' ] 445 | .forEach( function ( m ) { 446 | var _fn = _subtle[m]; 447 | 448 | _subtle[m] = function ( a, b, c ) { 449 | var args = [].slice.call(arguments); 450 | 451 | switch ( m ) { 452 | case 'exportKey': 453 | args[1] = b._key; 454 | break; 455 | case 'wrapKey': 456 | args[1] = b._key, args[2] = c._key; 457 | break; 458 | } 459 | 460 | if ( ( isWebkit || ( isIE && ( b.algorithm.hash || {} ).name === 'SHA-1' ) ) 461 | && m === 'exportKey' && a === 'jwk' && b.algorithm.name === 'HMAC' ) { 462 | args[0] = 'raw'; 463 | } 464 | 465 | if ( isWebkit && m === 'exportKey' && ( a === 'spki' || a === 'pkcs8' ) ) { 466 | args[0] = 'jwk'; 467 | } 468 | 469 | if ( isIE && m === 'wrapKey' ) { 470 | return _subtle.exportKey( a, b ) 471 | .then( function ( k ) { 472 | if ( a === 'jwk' ) k = s2b( unescape( encodeURIComponent( JSON.stringify( b2jwk(k) ) ) ) ); 473 | return _subtle.encrypt( args[3], c, k ); 474 | }); 475 | } 476 | 477 | var op; 478 | try { 479 | op = _fn.apply( _subtle, args ); 480 | } 481 | catch ( e ) { 482 | return Promise.reject(e); 483 | } 484 | 485 | if ( isIE ) { 486 | op = new Promise( function ( res, rej ) { 487 | op.onabort = 488 | op.onerror = function ( e ) { rej(e) }; 489 | op.oncomplete = function ( r ) { res(r.target.result) }; 490 | }); 491 | } 492 | 493 | if ( m === 'exportKey' && a === 'jwk' ) { 494 | op = op.then( function ( k ) { 495 | if ( ( isWebkit || ( isIE && ( b.algorithm.hash || {} ).name === 'SHA-1' ) ) 496 | && b.algorithm.name === 'HMAC') { 497 | return { 'kty': 'oct', 'alg': jwkAlg(b.algorithm), 'key_ops': b.usages.slice(), 'ext': true, 'k': s2a( b2s(k) ) }; 498 | } 499 | k = b2jwk(k); 500 | if ( !k.alg ) k['alg'] = jwkAlg(b.algorithm); 501 | if ( !k.key_ops ) k['key_ops'] = ( b.type === 'public' ) ? b.usages.filter(isPubKeyUse) : ( b.type === 'private' ) ? b.usages.filter(isPrvKeyUse) : b.usages.slice(); 502 | return k; 503 | }); 504 | } 505 | 506 | if ( isWebkit && m === 'exportKey' && ( a === 'spki' || a === 'pkcs8' ) ) { 507 | op = op.then( function ( k ) { 508 | k = jwk2pkcs( b2jwk(k) ); 509 | return k; 510 | }); 511 | } 512 | 513 | return op; 514 | } 515 | }); 516 | 517 | [ 'encrypt', 'decrypt', 'sign', 'verify' ] 518 | .forEach( function ( m ) { 519 | var _fn = _subtle[m]; 520 | 521 | _subtle[m] = function ( a, b, c, d ) { 522 | if ( isIE && ( !c.byteLength || ( d && !d.byteLength ) ) ) 523 | throw new Error("Empy input is not allowed"); 524 | 525 | var args = [].slice.call(arguments), 526 | ka = alg(a); 527 | 528 | if ( isIE && ( m === 'encrypt' || m === 'decrypt' ) && b.algorithm.hash ) { 529 | args[0].hash = args[0].hash || b.algorithm.hash; 530 | } 531 | 532 | if ( isIE && m === 'decrypt' && ka.name === 'AES-GCM' ) { 533 | var tl = a.tagLength >> 3; 534 | args[2] = (c.buffer || c).slice( 0, c.byteLength - tl ), 535 | a.tag = (c.buffer || c).slice( c.byteLength - tl ); 536 | } 537 | 538 | args[1] = b._key; 539 | 540 | var op; 541 | try { 542 | op = _fn.apply( _subtle, args ); 543 | console.log(op); 544 | } 545 | catch ( e ) { 546 | console.log(e); 547 | return Promise.reject(e); 548 | } 549 | 550 | if ( isIE ) { 551 | op = new Promise( function ( res, rej ) { 552 | op.onabort = 553 | op.onerror = function ( e ) { 554 | rej(e); 555 | }; 556 | 557 | op.oncomplete = function ( r ) { 558 | var r = r.target.result; 559 | 560 | if ( m === 'encrypt' && r instanceof AesGcmEncryptResult ) { 561 | var c = r.ciphertext, t = r.tag; 562 | r = new Uint8Array( c.byteLength + t.byteLength ); 563 | r.set( new Uint8Array(c), 0 ); 564 | r.set( new Uint8Array(t), c.byteLength ); 565 | r = r.buffer; 566 | } 567 | 568 | res(r); 569 | }; 570 | }); 571 | } 572 | 573 | return op; 574 | } 575 | }); 576 | 577 | if ( isIE ) { 578 | var _digest = _subtle.digest; 579 | 580 | _subtle['digest'] = function ( a, b ) { 581 | if ( !b.byteLength ) 582 | throw new Error("Empy input is not allowed"); 583 | 584 | var op; 585 | try { 586 | op = _digest.call( _subtle, a, b ); 587 | } 588 | catch ( e ) { 589 | return Promise.reject(e); 590 | } 591 | 592 | op = new Promise( function ( res, rej ) { 593 | op.onabort = 594 | op.onerror = function ( e ) { rej(e) }; 595 | op.oncomplete = function ( r ) { res(r.target.result) }; 596 | }); 597 | 598 | return op; 599 | }; 600 | 601 | global.crypto = Object.create( _crypto, { 602 | getRandomValues: { value: function ( a ) { return _crypto.getRandomValues(a) } }, 603 | subtle: { value: _subtle }, 604 | }); 605 | 606 | global.CryptoKey = CryptoKey; 607 | } 608 | 609 | if ( isWebkit ) { 610 | _crypto.subtle = _subtle; 611 | 612 | global.Crypto = _Crypto; 613 | global.SubtleCrypto = _SubtleCrypto; 614 | global.CryptoKey = CryptoKey; 615 | } 616 | })); -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /stats/get_count.php: -------------------------------------------------------------------------------- 1 | query("SELECT count(*) as count from stats;"); 5 | $value = $result->fetch_object(); 6 | print number_format($value->count); 7 | close($db_conn); 8 | ?> 9 | -------------------------------------------------------------------------------- /stats/index.php: -------------------------------------------------------------------------------- 1 | query("SELECT company, DATE(request_date) as request_date, COUNT(DISTINCT request_id) as requests from stats GROUP BY company, DATE(request_date) ORDER BY DATE(request_date) DESC"); 5 | ?> 6 | 7 | 8 | 11 | 14 | 17 | 18 | fetch_array()) 20 | { 21 | ?> 22 | 23 | 26 | 29 | 32 | 33 | 37 |
9 | Request Date 10 | 12 | Company 13 | 15 | Requests 16 |
24 | 25 | 27 | 28 | 30 | 31 |
38 | -------------------------------------------------------------------------------- /stats/private/.htaccess: -------------------------------------------------------------------------------- 1 | Deny from all -------------------------------------------------------------------------------- /stats/private/db/db_connection_template.php: -------------------------------------------------------------------------------- 1 | close(); 17 | } 18 | function install($mysqli){ 19 | $sql_statement = file_get_contents("install.sql"); 20 | if ($mysqli->query($sql_statement) == TRUE) { 21 | print("DB installation successful\n"); 22 | } 23 | } 24 | // $db_conn = connect(); 25 | // install($db_conn); 26 | // close($db_conn); 27 | -------------------------------------------------------------------------------- /stats/private/db/install.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS stats 2 | ( 3 | request_id VARCHAR(64), 4 | company VARCHAR(256), 5 | request_lang CHAR(2), 6 | request_date DATETIME, 7 | PRIMARY KEY (request_id) 8 | ); -------------------------------------------------------------------------------- /stats/private/get_token.php: -------------------------------------------------------------------------------- 1 | prepare("INSERT INTO stats (company, request_id, request_date, request_lang) VALUES (?, ?, ?, ?)"); 10 | $stmt->bind_param("ssss", $company, $hash, $now, $lang); 11 | $stmt->execute(); 12 | $inserted_rows = $stmt->affected_rows; 13 | $stmt->close(); 14 | close($db_conn); 15 | if($inserted_rows == 1){ 16 | return true; 17 | } 18 | else{ 19 | // Already had this hash 20 | return false; 21 | } 22 | } -------------------------------------------------------------------------------- /stats/private/validate_company.php: -------------------------------------------------------------------------------- 1 | 0){ 32 | if(in_array($_POST['ami_company'], $companies)){ 33 | $company_index = array_search($_POST['ami_company'], $companies); 34 | $sanitized_company = $companies[$company_index]; 35 | $DENIED = false; 36 | } 37 | } 38 | } 39 | if($DENIED){ 40 | return false; 41 | } 42 | else{ 43 | return $sanitized_company; 44 | } 45 | } -------------------------------------------------------------------------------- /stats/private/validate_hash.php: -------------------------------------------------------------------------------- 1 | $msg 33 | ); 34 | print json_encode($res); 35 | die(); 36 | } 37 | if(save_request($valid_company, $valid_hash, $valid_lang)){ 38 | // Valid token and form data 39 | $res = array( 40 | "msg" => "Thank you for your request to ".$valid_company."!" 41 | ); 42 | print json_encode($res); 43 | die(); 44 | } 45 | else{ 46 | $res = array( 47 | "msg" => "You already made this request before." 48 | ); 49 | print json_encode($res); 50 | die(); 51 | } --------------------------------------------------------------------------------