diff --git a/apps/i18n/common/en_us.json b/apps/i18n/common/en_us.json index 8ab1301d0d7b7..d750bfc729b8d 100644 --- a/apps/i18n/common/en_us.json +++ b/apps/i18n/common/en_us.json @@ -1007,8 +1007,10 @@ "shareSettingDisableDialogNote": "Note that students will still be able to share projects created using the programming tools designed for younger students like Play Lab and Artist. These tools limit what students can create and do not allow for uploading any of their own assets. To protect students’ privacy, shared creations in the project gallery are labeled only with the first letter of a student’s name and an age range.", "shareSettingMoreDetailsTooltip": "App Lab / Game Lab / Web Lab sharing enabled?", "shareWarningsAge": "Please provide your age below and click OK to continue.", - "shareWarningsMoreInfo": "Our Privacy Policy", - "shareWarningsStoreData": "This app built on Code Studio stores data that could be viewed by anyone with this sharing link, so avoid providing personal information about yourself or others.", + "shareWarningsMoreInfo": "Code.org Privacy Policy", + "shareWarningsStoreDataBeforeHighlight": "This app built on Code Studio stores data that can be viewed by anyone with access to this project, so ", + "shareWarningsStoreDataHighlight": "avoid providing personal information about yourself or others", + "shareWarningsStoreDataAfterHighlight": ".", "sharingAgePrompt": "Please select an age.", "sharingBlockedByTeacher": "Sorry, you do not have permissions to share this project. If you want to be able to share your project, please ask your teacher to enable sharing of App Lab / Game Lab / Web Lab projects for your section from the 'Manage students' tab in their dashboard. They can do this by adding the project sharing column from the Actions settings menu.", "show": "Show", diff --git a/apps/src/applab/Exporter.js b/apps/src/applab/Exporter.js index 0029c62144182..e949460089d9e 100644 --- a/apps/src/applab/Exporter.js +++ b/apps/src/applab/Exporter.js @@ -14,10 +14,12 @@ import exportFontAwesomeCssEjs from '../templates/export/fontAwesome.css.ejs'; import exportExpoIndexEjs from '../templates/export/expo/index.html.ejs'; import exportExpoPackageJson from '../templates/export/expo/package.exported_json'; import exportExpoAppJsonEjs from '../templates/export/expo/app.json.ejs'; -import exportExpoAppJs from '../templates/export/expo/App.exported_js'; +import exportExpoAppEjs from '../templates/export/expo/App.js.ejs'; import exportExpoCustomAssetJs from '../templates/export/expo/CustomAsset.exported_js'; +import exportExpoDataWarningJs from '../templates/export/expo/DataWarning.exported_js'; import exportExpoPackagedFilesEjs from '../templates/export/expo/packagedFiles.js.ejs'; import exportExpoPackagedFilesEntryEjs from '../templates/export/expo/packagedFilesEntry.js.ejs'; +import exportExpoWarningPng from '../templates/export/expo/warning.png'; import exportExpoIconPng from '../templates/export/expo/icon.png'; import exportExpoSplashPng from '../templates/export/expo/splash.png'; import logToCloud from '../logToCloud'; @@ -139,7 +141,7 @@ const APP_OPTIONS_OVERRIDES = { readonlyWorkspace: true, }; -export function getAppOptionsFile() { +export function getAppOptionsFile(expoMode) { function getAppOptionsAtPath(whitelist, sourceOptions) { if (!whitelist || !sourceOptions) { return null; @@ -155,6 +157,14 @@ export function getAppOptionsFile() { } const options = getAppOptionsAtPath(APP_OPTIONS_WHITELIST, getAppOptions()); _.merge(options, APP_OPTIONS_OVERRIDES); + options.nativeExport = expoMode; + if (!expoMode) { + // call non-whitelisted hasDataAPIs() function and persist as a bool in + // the exported options: + const { shareWarningInfo = {} } = getAppOptions(); + const { hasDataAPIs } = shareWarningInfo; + options.exportUsesDataAPIs = hasDataAPIs && hasDataAPIs(); + } return `window.APP_OPTIONS = ${JSON.stringify(options)};`; } @@ -230,14 +240,21 @@ function extractCSSFromHTML(el) { const fontAwesomeWOFFRelativeSourcePath = '/fonts/fontawesome-webfont.woff2'; const fontAwesomeWOFFPath = 'applab/fontawesome-webfont.woff2'; +function getExportConfigPath() { + const baseHref = project.getShareUrl(); + return `${baseHref}/export_config?script_call=setExportConfig`; +} + export default { exportAppToZip(appName, code, levelHtml, expoMode) { const { css, outerHTML } = transformLevelHtml(levelHtml); + const exportConfigPath = getExportConfigPath(); const jQueryBaseName = 'jquery-1.12.1.min'; var html; if (expoMode) { html = exportExpoIndexEjs({ + exportConfigPath, htmlBody: outerHTML, applabApiPath: "applab-api.j", jQueryPath: jQueryBaseName + ".j", @@ -250,6 +267,7 @@ export default { }); } else { html = exportProjectEjs({ + exportConfigPath, htmlBody: outerHTML, fontPath: fontAwesomeWOFFPath, }); @@ -287,6 +305,11 @@ export default { }); if (expoMode) { + appAssets.push({ + url: exportExpoWarningPng, + zipPath: appName + '/appassets/warning.png', + dataType: 'binary', + }); appAssets.push({ url: exportExpoIconPng, zipPath: appName + '/appassets/icon.png', @@ -307,11 +330,17 @@ export default { appName: appName, projectId: project.getCurrentId() }); + const { shareWarningInfo = {} } = getAppOptions(); + const { hasDataAPIs } = shareWarningInfo; + const appJs = exportExpoAppEjs({ + hasDataAPIs: hasDataAPIs && hasDataAPIs(), + }); zip.file(appName + "/package.json", exportExpoPackageJson); zip.file(appName + "/app.json", appJson); - zip.file(appName + "/App.js", exportExpoAppJs); + zip.file(appName + "/App.js", appJs); zip.file(appName + "/CustomAsset.js", exportExpoCustomAssetJs); + zip.file(appName + "/DataWarning.js", exportExpoDataWarningJs); } // NOTE: for expoMode, it is important that index.html comes first... zip.file(mainProjectFilesPrefix + "index.html", rewriteAssetUrls(appAssets, html)); @@ -339,7 +368,7 @@ export default { ))).then( ([applabApi], [commonLocale], [applabLocale], [applabCSS], [commonCSS], [fontAwesomeWOFF], ...rest) => { zip.file(appName + "/" + (expoMode ? "assets/applab-api.j" : rootApplabPrefix + "applab-api.js"), - [getAppOptionsFile(), commonLocale, applabLocale, applabApi].join('\n')); + [getAppOptionsFile(expoMode), commonLocale, applabLocale, applabApi].join('\n')); zip.file(mainProjectFilesPrefix + fontAwesomeWOFFPath, fontAwesomeWOFF); if (expoMode) { const [data] = rest[0]; @@ -453,26 +482,38 @@ export default { }, async publishToExpo(appName, code, levelHtml) { - const appOptionsJs = getAppOptionsFile(); + const appOptionsJs = getAppOptionsFile(true); const { css, outerHTML } = transformLevelHtml(levelHtml); const fontAwesomeCSS = exportFontAwesomeCssEjs({fontPath: fontAwesomeWOFFPath}); + const exportConfigPath = getExportConfigPath(); + const { origin } = window.location; + const applabApiPath = getEnvironmentPrefix() === 'cdo-development' ? + `${origin}/blockly/js/applab-api.js` : + `${origin}/blockly/js/applab-api.min.js`; const html = exportExpoIndexEjs({ + exportConfigPath, htmlBody: outerHTML, - commonLocalePath: "https://studio.code.org/blockly/js/en_us/common_locale.js", - applabLocalePath: "https://studio.code.org/blockly/js/en_us/applab_locale.js", + commonLocalePath: `${origin}/blockly/js/en_us/common_locale.js`, + applabLocalePath: `${origin}/blockly/js/en_us/applab_locale.js`, appOptionsPath: "appOptions.j", fontPath: fontAwesomeWOFFPath, - applabApiPath: "https://studio.code.org/blockly/js/applab-api.min.js", + applabApiPath, jQueryPath: "https://code.jquery.com/jquery-1.12.1.min.js", - commonCssPath: "https://studio.code.org/blockly/css/common.css", - applabCssPath: "https://studio.code.org/blockly/css/applab.css", + commonCssPath: `${origin}/blockly/css/common.css`, + applabCssPath: `${origin}/blockly/css/applab.css`, + }); + const { shareWarningInfo = {} } = getAppOptions(); + const { hasDataAPIs } = shareWarningInfo; + const appJs = exportExpoAppEjs({ + hasDataAPIs: hasDataAPIs && hasDataAPIs(), }); const appAssets = generateAppAssets({ html, code }); const files = { - 'App.js': { contents: exportExpoAppJs, type: 'CODE'}, + 'App.js': { contents: appJs, type: 'CODE'}, 'CustomAsset.js': { contents: exportExpoCustomAssetJs, type: 'CODE'}, + 'DataWarning.js': { contents: exportExpoDataWarningJs, type: 'CODE'}, }; const session = new SnackSession({ @@ -508,6 +549,13 @@ export default { filename: fontAwesomeWOFFPath, }); + appAssets.push({ + url: exportExpoWarningPng, + dataType: 'binary', + filename: 'warning.png', + assetLocation: 'appassets/', + }); + const assetDownloads = appAssets.map(asset => download(asset.url, asset.dataType || 'text') ); @@ -519,7 +567,7 @@ export default { const snackAssetUrls = await Promise.all(assetUploads); snackAssetUrls.forEach((url, index) => { - files['assets/' + appAssets[index].filename] = { + files[(appAssets[index].assetLocation || 'assets/') + appAssets[index].filename] = { contents: url, type: 'ASSET', }; @@ -604,7 +652,7 @@ function transformLevelHtml(levelHtml) { } function getEnvironmentPrefix() { - const hostname = window.location.hostname; + const { hostname } = window.location; if (hostname.includes("adhoc")) { // As adhoc hostnames may include other keywords, check it first. return "cdo-adhoc"; diff --git a/apps/src/applab/api-entry.js b/apps/src/applab/api-entry.js index a5ebf78d5ae84..b7840a1b3a753 100644 --- a/apps/src/applab/api-entry.js +++ b/apps/src/applab/api-entry.js @@ -15,6 +15,8 @@ import {getAppOptions, setAppOptions, setupApp} from '@cdo/apps/code-studio/init import {getStore} from '@cdo/apps/redux'; import {setIsRunning} from '@cdo/apps/redux/runState'; import {getExportedGlobals} from './export'; +import * as shareWarnings from '../shareWarnings'; +import {navigateToHref} from '../utils'; window.CDOSounds = Sounds.getSingleton(); const noop = function () {}; @@ -28,15 +30,16 @@ studioApp().highlight = noop; Applab.render = noop; // window.APP_OPTIONS gets generated on the fly by the exporter and appended to this file. -setAppOptions(Object.assign(window.APP_OPTIONS, {isExported: true})); +const exportOptions = Object.assign({isExported: true}, window.EXPORT_OPTIONS); +setAppOptions(Object.assign(window.APP_OPTIONS, exportOptions)); setupApp(window.APP_OPTIONS); -loadApplab(getAppOptions()); +const config = getAppOptions(); +loadApplab(config); // reset applab turtle manually (normally called when execution begins) // before the student's code is run. Applab.resetTurtle(); getStore().dispatch(setIsRunning(true)); - // Expose api functions globally, unless they already exist // in which case they are probably browser apis that we should // not overwrite... unless they are in a whitelist of browser @@ -50,26 +53,30 @@ for (let key in globalApi) { } } -const STORAGE_COMMANDS = [ - 'createRecord', - 'readRecords', - 'updateRecord', - 'deleteRecord', - 'onRecordEvent', - 'getKeyValue', - 'setKeyValue', - 'getKeyValueSync', - 'setKeyValueSync', -]; - -for (let key in STORAGE_COMMANDS) { - STORAGE_COMMANDS[key] = function () { - console.error("Data APIs are not available outside of code studio."); - }; -} - // Set up an error handler for student errors and warnings. injectErrorHandler(new JavaScriptModeErrorHandler( () => Applab.JSInterpreter, Applab )); + +function __start() { + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = 'code.js'; + document.getElementsByTagName("head")[0].appendChild(script); +} + +if (!config.nativeExport) { + document.addEventListener('DOMContentLoaded', () => { + if (config.exportUsesDataAPIs) { + shareWarnings.checkSharedAppWarnings({ + channelId: config.channel, + hasDataAPIs: () => ( true ), + onWarningsComplete: __start, + onTooYoung: () => ( navigateToHref('https://studio.code.org/too_young') ), + }); + } else { + __start(); + } + }); +} diff --git a/apps/src/code-studio/components/AdvancedShareOptions.jsx b/apps/src/code-studio/components/AdvancedShareOptions.jsx index 97b326ca4596c..f4d0eec2f0398 100644 --- a/apps/src/code-studio/components/AdvancedShareOptions.jsx +++ b/apps/src/code-studio/components/AdvancedShareOptions.jsx @@ -193,7 +193,6 @@ class AdvancedShareOptions extends React.Component {

Export your project as a zipped file, which will contain the HTML/CSS/JS files, as well as any assets, for your project. - Note that data APIs will not work outside of Code Studio.